Compare commits
994 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d25ab70fd | |||
| 44abe71542 | |||
| 098d444b32 | |||
| d50dcd8fb6 | |||
| 1c46df505c | |||
| 80ac5d713d | |||
| 37343dcbeb | |||
| 8329dc8141 | |||
| 96f293d240 | |||
| 4a00af18fe | |||
| 4e1ffd3ec4 | |||
| 1f4e2975cf | |||
| 7fbd064fd2 | |||
| 7f2d921136 | |||
| 7799c1f0dc | |||
| 2a93382700 | |||
| 2f8a00e1a5 | |||
| 058d667ac5 | |||
| 4b7c63206a | |||
| e989f78434 | |||
| e51ea247a8 | |||
| 38a78fa0b8 | |||
| 88ecdf85f3 | |||
| 114c781083 | |||
| 1755d6dae9 | |||
| 76baa05539 | |||
| f4e3559036 | |||
| 637751cf7c | |||
| c14eb1724f | |||
| 10e2dbbf04 | |||
| 0c0cdd2110 | |||
| 36ca5ca4cd | |||
| d629c8fc14 | |||
| a129a614e1 | |||
| cbab243e32 | |||
| 812c3d368f | |||
| f0d8a6b987 | |||
| 9bb52366ff | |||
| 172976e65e | |||
| 2ab3f1d0c7 | |||
| 6cd1858708 | |||
| 9ad9a91d11 | |||
| 4230f1542d | |||
| 69c13e1442 | |||
| 097830c082 | |||
| 8f59af0c64 | |||
| f375374525 | |||
| e06e2c2dbe | |||
| ebd898902d | |||
| 417eb9500d | |||
| 7e2fa61037 | |||
| b1d19ca43a | |||
| 25f1a2044f | |||
| ad88aa7b52 | |||
| 61ecb939dc | |||
| 28888e1de0 | |||
| 3310b692dc | |||
| 83633bdae3 | |||
| a262d8efe3 | |||
| 5bb91b773d | |||
| eab26a90a7 | |||
| 3edac3d2d6 | |||
| 0436afeec4 | |||
| 98626359aa | |||
| f50253f09f | |||
| fd433a07d6 | |||
| 32d2297700 | |||
| 45b8457f85 | |||
| 53362903c2 | |||
| f7c3ed9610 | |||
| 18328537ef | |||
| 6c516172bf | |||
| 38a0f33200 | |||
| a8cbd40a78 | |||
| c0fcaec52b | |||
| 0825623e7a | |||
| f367b9c5d8 | |||
| ae33724440 | |||
| 034945e8ea | |||
| 8f1da46986 | |||
| 7a344581af | |||
| d110b5ecfc | |||
| 4acd114e31 | |||
| 6532e30b1b | |||
| 47409585d3 | |||
| ee0431487f | |||
| 4ff2f56ffe | |||
| c518ed857e | |||
| fe2983ccba | |||
| c2ef089c07 | |||
| 2bb4a81f1c | |||
| e7eaf3a4b2 | |||
| 496d254fcf | |||
| aa70c13985 | |||
| 675a760e44 | |||
| c095c39b1c | |||
| 6c0f9d6d8d | |||
| ae3397f9a9 | |||
| 502a38f770 | |||
| 3375bac60c | |||
| 3c495a12ee | |||
| 9ec0cb2be6 | |||
| f537e2e27a | |||
| 4df711c7d1 | |||
| 009b982060 | |||
| 45e818f691 | |||
| 1ef8f6dcbb | |||
| e7585e09ec | |||
| 01b019dc80 | |||
| 3fcde2c55e | |||
| a4f52b2064 | |||
| 129f9957e5 | |||
| 3428ff6f3d | |||
| 25a8c5476b | |||
| e76920fa6b | |||
| 4ed56ed38d | |||
| 81f972aefb | |||
| db25fa357b | |||
| 6034ce8e0c | |||
| 73aed95d87 | |||
| 2ac13ef9db | |||
| e6ba070e55 | |||
| 5eea3c10f7 | |||
| d346e72c98 | |||
| a4824d6973 | |||
| 205a2448f3 | |||
| 712c9a78ff | |||
| 56d92ccd67 | |||
| b039b38e61 | |||
| 8ea300b777 | |||
| 3a5b2d03ac | |||
| 485c6c9d90 | |||
| 1d20bf3e79 | |||
| 53af53a01c | |||
| 2adb88e9a1 | |||
| 869cfdffcd | |||
| 27a768e10d | |||
| 918fa71a1c | |||
| bf1de55c8d | |||
| fdf407a3e4 | |||
| e5b4b7296c | |||
| 268bd594eb | |||
| 0ad8f01404 | |||
| 378d5dc764 | |||
| 20a0ec1f0e | |||
| ad9b42df41 | |||
| 0130c4e456 | |||
| 05e7f12db1 | |||
| 3e9a209552 | |||
| 8d338ca2c6 | |||
| 37124ba29a | |||
| 4f4ca63454 | |||
| f0406d8181 | |||
| 0f981717dd | |||
| 9ecf416fb0 | |||
| 3c132527f9 | |||
| 54e5da26d1 | |||
| 0bdbced718 | |||
| 1571490408 | |||
| 366798154b | |||
| 7fd9087c56 | |||
| b0f98485ca | |||
| 61d836b01d | |||
| b3620c23fb | |||
| 29a32041a8 | |||
| 84f609dda4 | |||
| 0826f1a858 | |||
| 210e578157 | |||
| dd5055ea20 | |||
| c87a429b2a | |||
| 1a8659cdaa | |||
| 3faffa65ff | |||
| ae5b86a3af | |||
| 4a0cd0a61c | |||
| 8c26e456c6 | |||
| 647a91cea3 | |||
| 6dd7302e7f | |||
| 3af7049234 | |||
| 619c76c96d | |||
| b09b4dc0ae | |||
| e1811ae163 | |||
| 784bb22672 | |||
| ba370693eb | |||
| 7117c90cda | |||
| 53848b31bf | |||
| 206c4c8b84 | |||
| 33dd5ac6c8 | |||
| 8ff3d62b5e | |||
| 2ff7d190a7 | |||
| 2227078c9a | |||
| ccf44242c3 | |||
| 6933a8e0fc | |||
| 5009ba6862 | |||
| 0daf1b6aab | |||
| 1dcf5fba85 | |||
| 2608425343 | |||
| e5e7cc2c5b | |||
| f5f084f663 | |||
| 2107e0942c | |||
| ff25e6ec87 | |||
| da13c7e71e | |||
| 88f5bcd81f | |||
| 7b62aa5ff4 | |||
| f05abf53f3 | |||
| 9741c79b03 | |||
| 5f5d6c230b | |||
| f1d0f7cff2 | |||
| e9c959371b | |||
| 1533a04680 | |||
| b61234cea9 | |||
| 3e41df5c9e | |||
| 2d7ebe83a0 | |||
| f00537afae | |||
| 4870438dcd | |||
| 13c79a9814 | |||
| 843a7bfa98 | |||
| a106b45e0d | |||
| 333b2652f5 | |||
| 8a0e87fc46 | |||
| b1083f2bf5 | |||
| d8a84a02eb | |||
| a403e7afcc | |||
| 59e68349dc | |||
| 43bb3a1b35 | |||
| 4977ed62c9 | |||
| 6adaf05680 | |||
| a70d99a3a9 | |||
| db41db65fa | |||
| 3e40878a88 | |||
| e74d129e83 | |||
| 4708674033 | |||
| 54af72adf1 | |||
| 6fee4c32ba | |||
| f99fc3eb56 | |||
| cccd2b2480 | |||
| 83bdbd658b | |||
| 28f4c2235d | |||
| b3ae69df9d | |||
| e72312a403 | |||
| b2d51aa6ab | |||
| d0098ae093 | |||
| b378f2ff77 | |||
| 5ce0e14ea6 | |||
| 8698ab9431 | |||
| 900d046c24 | |||
| 66f882eac8 | |||
| 9a8333e537 | |||
| 237eaf85cb | |||
| 887b3f3f3a | |||
| fee294e35b | |||
| 9df9d81b64 | |||
| c37daf3b8d | |||
| ae63ebe2e8 | |||
| 38f1bc1546 | |||
| b9e4a8783a | |||
| 6045ea3988 | |||
| 373557030d | |||
| 1434338774 | |||
| ddf42cb705 | |||
| 0757c70445 | |||
| 59e0729ec1 | |||
| 564c906ebb | |||
| 757b593355 | |||
| 6ee24c0de2 | |||
| 7e22e06fc8 | |||
| 6629ee671f | |||
| ed3bcc3669 | |||
| c93bf66fcb | |||
| 5af4ea1a2f | |||
| 1bcf7c12ed | |||
| 43c8aec9bc | |||
| 54aac0b785 | |||
| 705231857c | |||
| 044837b871 | |||
| 0e0a88e516 | |||
| 5246860a12 | |||
| 5cc55ed0c0 | |||
| 52fb8a8d8f | |||
| 359e970083 | |||
| 82fa5fffda | |||
| 658c850a60 | |||
| 543447b31b | |||
| 2a2211d0c7 | |||
| 4ea20a0f43 | |||
| 133cf4f979 | |||
| a7920e0b27 | |||
| 80bf70a9d4 | |||
| 53c3ac1777 | |||
| 6eb47ebd6e | |||
| dc161feb81 | |||
| 82783a7cc6 | |||
| 628b90a284 | |||
| 7a07039ad7 | |||
| 35b0d90fd2 | |||
| f9cf8c533c | |||
| 5502d5a2e1 | |||
| f6b39f795a | |||
| 52302c1453 | |||
| c40d37ec52 | |||
| de9b06d9f3 | |||
| 6afd93b403 | |||
| 92c24d031a | |||
| 4a00f8a5c3 | |||
| daba7d782c | |||
| 69dd7918da | |||
| 0349f7d60f | |||
| b808c5d340 | |||
| 0b24961e14 | |||
| 9f33f30f6a | |||
| dbd2eed8bc | |||
| a4632f1a55 | |||
| adf0d3fda8 | |||
| 22987f8e75 | |||
| 4290885b2e | |||
| 8c523c4f45 | |||
| f6b861e5e7 | |||
| 5a747a2c5b | |||
| 908e06a043 | |||
| 75060011db | |||
| 4c66355607 | |||
| 898a55c4d8 | |||
| 2ca2eac30a | |||
| eff4dedc47 | |||
| bc28d61c9b | |||
| f9119a6fc6 | |||
| 077dc119b0 | |||
| 1cfbf7c301 | |||
| fe508cdc0d | |||
| 294da3e1bc | |||
| bf0c10fc79 | |||
| f1f2df98e2 | |||
| 4a34d2855a | |||
| 2d9b08d462 | |||
| b5c803d75f | |||
| c9e77d408a | |||
| 667c30fb3a | |||
| bf197fbd0f | |||
| a7cbdcc6bd | |||
| 29831a5a38 | |||
| 6b9cd41461 | |||
| 04c3852e15 | |||
| eadd1acd57 | |||
| 92ebea092f | |||
| 24d6a652b5 | |||
| 64c9feaf63 | |||
| f4ae4ee256 | |||
| a8d8a09660 | |||
| 4d5a2a1f11 | |||
| 6c42da91fc | |||
| 877f127b39 | |||
| 2ce1c95de0 | |||
| b0032caef4 | |||
| 15df4b5a89 | |||
| c3c6af9680 | |||
| e0df66f2d7 | |||
| 5b2ac47aa9 | |||
| 52f4e19cab | |||
| 7ebc0dd98e | |||
| f333176ea4 | |||
| bd69b8a0d4 | |||
| df145c3642 | |||
| 89f6383f46 | |||
| 7ca05c249c | |||
| ff3be1ccb8 | |||
| 3fd43091d7 | |||
| 6bf6ebd3c8 | |||
| 8cf0e58adb | |||
| bf33cd262b | |||
| 191973a988 | |||
| 5c5a812e6e | |||
| 97037d059d | |||
| 32eae5997d | |||
| c31cf70e60 | |||
| e4e39e99b3 | |||
| 8173153a96 | |||
| 496543eefd | |||
| 1b14efee19 | |||
| 0580f7e799 | |||
| 89f3171525 | |||
| b845777235 | |||
| ac10a7b084 | |||
| 40f3816d19 | |||
| 5aa8a804b7 | |||
| 590275cbc9 | |||
| 5c2fbbbe76 | |||
| 2cfd9b366b | |||
| 5f238a8d89 | |||
| d54c09f2a8 | |||
| 4d1b8aa407 | |||
| 100d21e7b3 | |||
| 9525632ada | |||
| d2d05531f8 | |||
| eab3a138af | |||
| 4407fa25f4 | |||
| db42383830 | |||
| 721729577a | |||
| d046b9d32f | |||
| 6c04cf4d2d | |||
| d2d46b27db | |||
| f8b71770a9 | |||
| 8cb0b44b94 | |||
| 4de3925110 | |||
| f55a499676 | |||
| 716a3e318d | |||
| d72fb97041 | |||
| 34553d8da3 | |||
| e5bd9b894d | |||
| db0aebe7ca | |||
| 522bb0f5e8 | |||
| 971758ccee | |||
| bc0edd3b8a | |||
| c47187c18e | |||
| 392086f671 | |||
| 244ad5568b | |||
| 41927597f0 | |||
| f6a56a61be | |||
| b533cd4266 | |||
| f8b9642e55 | |||
| 125b8220a4 | |||
| d4a57af24d | |||
| 9b42956b0f | |||
| d715f2616e | |||
| b14c80af1c | |||
| bc8e7abf26 | |||
| 8484e74dea | |||
| 63e9a34d73 | |||
| 20a0380e47 | |||
| ff2310a0de | |||
| 244ccd1b03 | |||
| 9d0650d555 | |||
| 3039834e85 | |||
| 5f7b91f233 | |||
| bb77b06047 | |||
| 33b252b533 | |||
| fb5336bf15 | |||
| c15340d5fd | |||
| 82515da893 | |||
| c2fa700dc1 | |||
| 9e9794ece3 | |||
| eb7167bb4f | |||
| 546af4b7c6 | |||
| 6df8c63144 | |||
| 445c968279 | |||
| d54d57ec63 | |||
| e79e8899a1 | |||
| 8c453beeca | |||
| d7c95f285e | |||
| 5f5df223e8 | |||
| 115a1a1c1f | |||
| 28ffd9510d | |||
| bc39bce60d | |||
| 080088e8eb | |||
| 47df8696a6 | |||
| f322f5e1d3 | |||
| 01c8e764b2 | |||
| 119927b565 | |||
| 5a31ae302f | |||
| b7f4a3a574 | |||
| 83792fd623 | |||
| 6993af2416 | |||
| 6385bc9efe | |||
| 551934cf6a | |||
| f03a255314 | |||
| 6d07da5661 | |||
| 5c51243f9c | |||
| 7df98ae282 | |||
| ab5f2336bc | |||
| a1a65012f2 | |||
| 43b01089cc | |||
| 2d2ae2f269 | |||
| 1473468b4c | |||
| e01a0b1b8b | |||
| d2602b765d | |||
| 885e136f16 | |||
| f241b7047c | |||
| 25a286b0b7 | |||
| b66ce33ced | |||
| 3083097885 | |||
| e9df744b5e | |||
| 62799acdb6 | |||
| c21c7917ce | |||
| 47bd0b446a | |||
| fc48e88628 | |||
| 9738ad3029 | |||
| 5e9a47ec2e | |||
| 97f3d1be35 | |||
| f76f989937 | |||
| 7f916a402f | |||
| f0a1faa3ab | |||
| 6616c8fc88 | |||
| d72e82e156 | |||
| 3eda5a6c5c | |||
| 85e989b024 | |||
| 26ea905cae | |||
| c8e5c25579 | |||
| b686783c84 | |||
| 337e8d6da9 | |||
| d079830824 | |||
| ef26867bce | |||
| fd5c81cb96 | |||
| f16c0cbb2c | |||
| 9e93f161df | |||
| df49ad34f7 | |||
| cf636d5b2d | |||
| 845075fb5b | |||
| c69ce3e1b9 | |||
| 784fd069b2 | |||
| 990830213d | |||
| f1dc6c9189 | |||
| e944bde4b9 | |||
| e05a450b1f | |||
| ad403cf0e1 | |||
| ffe11ff7d2 | |||
| 5695064fd6 | |||
| c8b88d36c3 | |||
| 7d123764be | |||
| 34f95c35cc | |||
| 875e3d75ff | |||
| 3b40564ebe | |||
| 2f9da25028 | |||
| 01535692cf | |||
| cb996aee38 | |||
| d755438db3 | |||
| d38bf8ac5f | |||
| 0fc15712ca | |||
| ef1b2eeb88 | |||
| 7adfba1d25 | |||
| 84ede7d286 | |||
| c0df4475f9 | |||
| 8cb38f19ee | |||
| f59be0bbfc | |||
| dfa9d65201 | |||
| 2960fececf | |||
| ccbc30f1b2 | |||
| a1c9221feb | |||
| a132207741 | |||
| b1b7c92177 | |||
| 87f3416ff6 | |||
| 1cdc12e5cd | |||
| 5f7f48a3d5 | |||
| 960c6f311d | |||
| f244d173da | |||
| e8b78194ab | |||
| b3343588a8 | |||
| 71a512717c | |||
| c7b99eb58d | |||
| a6b31edde9 | |||
| 22128ad609 | |||
| 2218679430 | |||
| 6720d8e98e | |||
| ffebf6f7aa | |||
| 8ab8f315e4 | |||
| 8d0cd68cf1 | |||
| 608370d289 | |||
| 3be780ed1b | |||
| 67f15ed17d | |||
| 31b32ada8e | |||
| 6c065a7250 | |||
| c04537b77a | |||
| 32853142de | |||
| a64c8168ac | |||
| 34d2b25be5 | |||
| 92d9d6cd0d | |||
| 3687e57cbd | |||
| 3253864d4c | |||
| 2cd9fd2406 | |||
| 73fd0fa17e | |||
| b6b1065517 | |||
| 39d32e52ae | |||
| d5ace59c9b | |||
| 290d512537 | |||
| cd00f2aa3e | |||
| f09d1e5f8f | |||
| df192b8e25 | |||
| 665d761265 | |||
| 812c71dc7a | |||
| 4f4428a647 | |||
| 3a106af89c | |||
| e755eacd46 | |||
| 785662faab | |||
| 02dc8dd179 | |||
| 308e9c4bb3 | |||
| d70c4c642a | |||
| 02f6b226fb | |||
| 9bc1a5ef37 | |||
| ef7491bd2d | |||
| 00ccf118be | |||
| 7b3ef0c5b0 | |||
| c8d6fdf321 | |||
| eb7812761b | |||
| 657a32f1e1 | |||
| 4ab7cf10ea | |||
| 141a5d41f7 | |||
| b4992b8186 | |||
| 8043b3fe53 | |||
| d210e4ed28 | |||
| 1aec69d936 | |||
| 8ce2f5c94c | |||
| 0800b99579 | |||
| 638390af46 | |||
| 32398dc581 | |||
| f0c6d6f163 | |||
| 9c83cbc0f0 | |||
| 872757a6ad | |||
| 6bdbde18b5 | |||
| c295e141e1 | |||
| 314507c5d1 | |||
| ac62f192d6 | |||
| 28a9c19435 | |||
| f3c1b13d38 | |||
| 8907a558c5 | |||
| ba4c9472c2 | |||
| 325717871f | |||
| 7119c0b95e | |||
| 41b3ef58de | |||
| 7d5f71d8b7 | |||
| e8cc186265 | |||
| d3a5df905e | |||
| e7212c8eb6 | |||
| 87df0b2865 | |||
| 6e52dc0cac | |||
| 49f51459c5 | |||
| d38bf619a5 | |||
| 7d8760ead1 | |||
| 1799582f1f | |||
| 1eb4046261 | |||
| 66ce235d30 | |||
| b897498356 | |||
| 7ce28bea47 | |||
| f46f69c0a1 | |||
| 0765240fb4 | |||
| 491facec2e | |||
| 8976ddab13 | |||
| 5db2734513 | |||
| 51ab7ebf4b | |||
| 8f120e826c | |||
| c10f7fed99 | |||
| 859d1879cf | |||
| f60c1e1326 | |||
| 1d754b82b9 | |||
| 1f176bd719 | |||
| cf58b4e07a | |||
| b41371f917 | |||
| 7f3298dc15 | |||
| 7f178ddc61 | |||
| 5611bdec4a | |||
| 5d1c9810e7 | |||
| d118835c46 | |||
| 56934ab064 | |||
| 6e443ce487 | |||
| ccb204aca8 | |||
| 0ab4405951 | |||
| 5d109caf19 | |||
| 1d1c3f1df4 | |||
| 9e9e90a9df | |||
| fa5f421521 | |||
| 7642974a3b | |||
| 571587ceac | |||
| a147e99933 | |||
| 956a3e9acb | |||
| 1bbac4f879 | |||
| b05b5dd788 | |||
| fc08dbdefe | |||
| a7177656a4 | |||
| 07a43c119e | |||
| eeed2ac7cd | |||
| 3b18df78b2 | |||
| 835fa2b9e7 | |||
| cff5e73e4b | |||
| 5087d4c985 | |||
| d023f46031 | |||
| ec37a8f49f | |||
| 94c22a6928 | |||
| 2c91ac1be1 | |||
| 9e56c46717 | |||
| 5ae825cec2 | |||
| f47813face | |||
| 5b5ea22964 | |||
| a89b2e1aa0 | |||
| 09aca8cf54 | |||
| 2672888031 | |||
| 9c714ffaed | |||
| 2161843876 | |||
| e4686cb9ee | |||
| a25886cc9f | |||
| 0ab2e14ba9 | |||
| e39b33de04 | |||
| a48aaafb92 | |||
| 68613b8e5b | |||
| ba061cf47a | |||
| 4d857b758a | |||
| 64f3d6b0ae | |||
| 8a7c529ede | |||
| 0e5691ef18 | |||
| c1f877932d | |||
| ae88a3004d | |||
| b7b4ffd2f0 | |||
| 4ff0b9405d | |||
| 22379515e3 | |||
| d7156fa614 | |||
| 59f5760708 | |||
| b44598b685 | |||
| 41e4a95556 | |||
| c8172b605c | |||
| 638ffee508 | |||
| 6982b40688 | |||
| 027585f737 | |||
| acb64b9066 | |||
| a857cc862e | |||
| 6821823fbd | |||
| 08fa2687e6 | |||
| 75ffed8918 | |||
| 379d0af081 | |||
| 3dc8efe5e1 | |||
| 35c4841d19 | |||
| 4f352a4dad | |||
| 873622e3e4 | |||
| 233858f447 | |||
| 7707781f0f | |||
| 4e6928edd4 | |||
| c755282408 | |||
| d28959a637 | |||
| 2b197551d6 | |||
| 01539ffdf4 | |||
| f00190aa20 | |||
| 1c585cd307 | |||
| bc67fa9e66 | |||
| a5d8c2bf14 | |||
| 5df85eeb64 | |||
| 43b22f36b9 | |||
| 33f150df11 | |||
| 4463fa3a94 | |||
| d5440e93f2 | |||
| 35d7cb1abc | |||
| de22a3fd1f | |||
| d4de119db4 | |||
| 7cf33cb8fb | |||
| 0ef72929f4 | |||
| adb9e6fcca | |||
| 0bf229c5ef | |||
| c589742201 | |||
| 3e4b8a43b8 | |||
| 0dd6521a9d | |||
| 05878ed7b3 | |||
| b9683050fb | |||
| b1e7d48714 | |||
| 57181398dc | |||
| d970f349c2 | |||
| e65790ed24 | |||
| b983d57a39 | |||
| d74f71c49e | |||
| 5fb7d29f78 | |||
| 3a17b27abf | |||
| fda723fdb7 | |||
| 4efa758797 | |||
| d138a2c01e | |||
| 3e652d8777 | |||
| 06d6005ffb | |||
| 3a2f0e7114 | |||
| 4d635b4933 | |||
| a7c233c0a8 | |||
| 29f11a6074 | |||
| b1ab51b68a | |||
| 9fa925fd16 | |||
| 8ce5bc60b7 | |||
| f9e6b5e3d2 | |||
| 3b8d7d3d06 | |||
| d887c49b53 | |||
| 20c6c26764 | |||
| 7b81836b21 | |||
| 289c52c2cb | |||
| 66de14d3cf | |||
| 0f8d4c381a | |||
| 6d0bf7fffa | |||
| 30a8f0470e | |||
| 01f0587b42 | |||
| 9fe76ea4eb | |||
| 810de4d160 | |||
| 61abe5d8bc | |||
| ae41315c8f | |||
| 4fb5d7787b | |||
| 888c5260eb | |||
| dbe1c98c13 | |||
| 927e31471c | |||
| 520ee79498 | |||
| 32f2bb98e1 | |||
| ebc1121c3f | |||
| d255cc342b | |||
| 73004f638b | |||
| a154ba9e0d | |||
| f2d6ae813c | |||
| 0690e61da1 | |||
| c13b3fb110 | |||
| b521c8bdd1 | |||
| 03cf85dd7a | |||
| 0bdba06181 | |||
| e6f6227e59 | |||
| f7d592aecc | |||
| 13876fbb69 | |||
| 14e3b5a933 | |||
| 5d7c79e168 | |||
| 08c7844047 | |||
| 75e7a4650d | |||
| 2f9d102d02 | |||
| 465cb9286f | |||
| cb0be62c40 | |||
| 30784e0740 | |||
| 283353a71a | |||
| a934d3ead7 | |||
| 1464475df2 | |||
| a4cfef1740 | |||
| ecee044716 | |||
| 0bd2b10706 | |||
| e0cf29a07a | |||
| feffd8a93c | |||
| 3c050db5c3 | |||
| 8c615d6233 | |||
| 194bbd5996 | |||
| 3002f6bb99 | |||
| 022c085241 | |||
| d71b78b20b | |||
| 6627d1020a | |||
| 9454df0585 | |||
| 3cd2facb7f | |||
| 1e2d460583 | |||
| 6bb35a2494 | |||
| 7ab16f4858 | |||
| f14bb05f1f | |||
| 0a1145fce7 | |||
| 749fd6caf5 | |||
| 8db94647df | |||
| 7bf5b07f60 | |||
| 31fefe12ac | |||
| 05d3ea95da | |||
| d7b1fb3e7a | |||
| eb97c5d393 | |||
| 39c2bab09f | |||
| 88c1115034 | |||
| ab6171d6c9 | |||
| 19f9f6ca81 | |||
| aecd86b56b | |||
| ed5ddb3d1f | |||
| f724bb7951 | |||
| 47f085b0ce | |||
| 024307a329 | |||
| d25abace00 | |||
| 84bc3cf9a9 | |||
| 900da199cc | |||
| 7ce8778c7d | |||
| 46abdcc528 | |||
| af75cf7116 | |||
| 46b3d86dbb | |||
| 0b61819b2d | |||
| 21eede5c27 | |||
| 5d284ddb55 | |||
| 9b3ea0bf7c | |||
| f89656b582 | |||
| 7691c30bd5 | |||
| 622b02bb80 | |||
| 9156cece88 | |||
| 256e4339f5 | |||
| 08e85a3827 | |||
| 85969a537b | |||
| 0e8a742dd9 | |||
| e9b8d6c6a5 | |||
| c103d1d708 | |||
| 279890f962 | |||
| f4b1be8330 | |||
| f0f05357ca | |||
| 2d6a628391 | |||
| cfef0bfc4b | |||
| 9943bbe4b7 | |||
| 4f26d270ed | |||
| 6c66be57a6 | |||
| 7bc643c889 | |||
| b77c8ed13d | |||
| 7e6d9ff4a2 | |||
| d237a6a7f3 | |||
| ec63c8e3ff | |||
| bc1960f1a5 | |||
| b3b2a4a09c | |||
| 7f02c0fa8a | |||
| 6d0b72ebd4 | |||
| 9972e8b2b4 | |||
| 22cef958c6 | |||
| c234ed3f38 | |||
| 1c810f3e28 | |||
| 6043696ca7 | |||
| e7f81e24dc | |||
| 28a2806d4b | |||
| 9683bef22e | |||
| 3e53eebcf4 | |||
| b406431755 | |||
| caf297283c | |||
| 9f7c200e69 | |||
| 4f26d0e120 | |||
| 039a4028a3 | |||
| e11a167a55 | |||
| 4639de7157 | |||
| 1462653763 | |||
| ebdabfefb4 | |||
| 560dc17883 | |||
| ea715bc2b8 | |||
| 1189277f46 | |||
| 73e017bb6f | |||
| 11e6c3f440 | |||
| 50a97c0729 | |||
| b991170aec | |||
| 6d1fbd3152 | |||
| 07005ec619 | |||
| 818ac2f32e | |||
| 246418367f | |||
| 2b5ff2c672 | |||
| 1e032ed819 | |||
| a7112fa127 | |||
| b529c1b275 | |||
| 345989463f | |||
| fbf2a69203 | |||
| b45918c4c3 | |||
| 60410ea046 | |||
| 9db18e74b7 | |||
| 954fbea6ba | |||
| 402016da36 | |||
| 2fac69e1f7 | |||
| 05c32f93ab | |||
| 0e8d8e2449 | |||
| d55748d291 | |||
| 612114beaf | |||
| 6a1d232347 | |||
| 437be3d61d | |||
| 008698943a | |||
| 62b5767bc9 | |||
| 8496265084 | |||
| a3ee86b65b | |||
| 2379887c93 | |||
| 1e029580c6 | |||
| e52826f731 | |||
| b428739a19 | |||
| 867147829b | |||
| 42c6125e94 | |||
| 63b0b66739 | |||
| cfdb8726d8 | |||
| b04295fa90 | |||
| 77799aadcc | |||
| f51e399dcb | |||
| 63b371b678 | |||
| d26692fbf2 | |||
| 0034dc39b5 | |||
| 36e73e4ad2 | |||
| 05ed853084 | |||
| 59bf6f13fe | |||
| dcc5c5cbbf | |||
| 1803c8e297 | |||
| 14f7cfadf8 | |||
| 1d93108df9 | |||
| cde0e0724c | |||
| ee712a0c8c | |||
| 721da5e226 | |||
| e3c658bd79 | |||
| 3b49211aab | |||
| b991f8fe10 | |||
| 9fbeddf8f4 | |||
| 252bcb5de3 | |||
| 4ef6246284 | |||
| a253e6524a | |||
| f34113302f | |||
| 1c287d3a0f | |||
| 29064d6ada | |||
| c95f80dd75 | |||
| 0e9258c300 | |||
| 08a9c26ed2 | |||
| 1dbdb88717 | |||
| cd1715e703 | |||
| d9fe66299a | |||
| e1741b528e | |||
| 68bf2e9be8 | |||
| 266ffe9b9e | |||
| a46ef57a60 | |||
| 6505f7601f | |||
| 9684a58dbc | |||
| f50c843cb7 | |||
| 7f34e89107 | |||
| 4315ccba98 | |||
| 6759e17f1c | |||
| b1d591c946 | |||
| fad9ea6a5d | |||
| 5e5d20b705 | |||
| fa5e180371 | |||
| ba3a120f6d | |||
| 8c54e612ac | |||
| 239119a12e | |||
| 34e529cc70 | |||
| e2a494e39d | |||
| cee1e2d521 |
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "entire hooks claude-code post-task"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "TodoWrite",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "entire hooks claude-code post-todo"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "entire hooks claude-code pre-task"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "entire hooks claude-code session-end"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "entire hooks claude-code session-start"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "entire hooks claude-code stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "entire hooks claude-code user-prompt-submit"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Read(./.entire/metadata/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ dist
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
@@ -23,10 +24,11 @@ dist
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.sentry-build-plugin
|
||||
|
||||
# Docker
|
||||
compose.yml
|
||||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
|
||||
# Development files
|
||||
.github
|
||||
@@ -38,6 +40,8 @@ packages
|
||||
# Test files
|
||||
coverage
|
||||
.nyc_output
|
||||
vitest.config.ts
|
||||
vitest.setup.ts
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
tmp/
|
||||
settings.local.json
|
||||
metadata/
|
||||
logs/
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"strategy": "manual-commit",
|
||||
"enabled": true,
|
||||
"telemetry": true
|
||||
}
|
||||
@@ -66,5 +66,51 @@ CRON_SECRET="your-secret-key-here"
|
||||
# SENTRY_AUTH_TOKEN=sentry-auth-token
|
||||
|
||||
# GOOGLE_ADSENSE_ACCOUNT=ca-pub-xxxxxxxxxxxxxxxx
|
||||
# EZOIC_ENABLED=true
|
||||
# EZOIC_SITE_DOMAIN=prompts.chat
|
||||
|
||||
# ==============================================================================
|
||||
# SSO Authentication Configurations
|
||||
#
|
||||
# Why two generic providers (OIDC vs OAuth 2.0)?
|
||||
# - "OIDC" (OpenID Connect) performs strict cryptographic validation on the
|
||||
# ID Token (verifying 'aud' matching the client ID, 'iss' matching issuer, etc).
|
||||
# Use this for standard providers like Google, Auth0, Okta, Keycloak.
|
||||
# - "OAuth 2.0" bypasses strict OIDC validation. Use this for legacy or
|
||||
# enterprise systems that do not return OIDC ID Tokens
|
||||
# ==============================================================================
|
||||
|
||||
# Generic OIDC Config (Strict Validation)
|
||||
# ⚠️ Ensure you add "oidc" to the `providers` array in your prompts.config.ts file
|
||||
# Callback URL to whitelist: <your-domain>/api/auth/callback/oidc
|
||||
# AUTH_OIDC_ID="dummy-oidc-client-id"
|
||||
# AUTH_OIDC_SECRET="dummy-oidc-client-secret"
|
||||
# AUTH_OIDC_ISSUER="https://oidc.example.com"
|
||||
# AUTH_OIDC_WELLKNOWN="https://oidc.example.com/.well-known/openid-configuration"
|
||||
# AUTH_OIDC_SCOPE="openid email profile"
|
||||
# AUTH_OIDC_NAME="Company OIDC"
|
||||
# Optional overrides (uncomment to use):
|
||||
# AUTH_OIDC_LOGO="https://your-domain.com/oidc-logo.png" # Local path or full URL to button image
|
||||
# AUTH_OIDC_AUTHORIZATION_URL="https://oidc.example.com/authorize"
|
||||
# AUTH_OIDC_TOKEN_URL="https://oidc.example.com/token"
|
||||
# AUTH_OIDC_USERINFO_URL="https://oidc.example.com/userinfo"
|
||||
# AUTH_OIDC_JWKS_URL="https://oidc.example.com/jwks"
|
||||
# AUTH_OIDC_TOKEN_AUTH_METHOD="client_secret_post" # Allowed values: "client_secret_basic", "client_secret_post", "none"
|
||||
# AUTH_OIDC_ENABLE_PKCE="true" # PKCE is enabled by default. Set to "false" to disable.
|
||||
|
||||
# Generic OAuth 2.0 Config (Loose Validation)
|
||||
# ⚠️ Ensure you add "oauth" to the `providers` array in your prompts.config.ts file
|
||||
# Callback URL to whitelist: <your-domain>/api/auth/callback/oauth
|
||||
# AUTH_OAUTH_ID="dummy-oauth-client-id"
|
||||
# AUTH_OAUTH_SECRET="dummy-oauth-client-secret"
|
||||
# AUTH_OAUTH_ISSUER="https://sso.example.com"
|
||||
# AUTH_OAUTH_WELLKNOWN="https://sso.example.com/.well-known/openid-configuration"
|
||||
# AUTH_OAUTH_SCOPE="email profile"
|
||||
# AUTH_OAUTH_NAME="Company SSO"
|
||||
# Optional overrides (uncomment to use):
|
||||
# AUTH_OAUTH_LOGO="https://your-domain.com/sso-logo.png" # Local path or full URL to button image
|
||||
# AUTH_OAUTH_AUTHORIZATION_URL="https://sso.example.com/authorize"
|
||||
# AUTH_OAUTH_TOKEN_URL="https://sso.example.com/token"
|
||||
# AUTH_OAUTH_USERINFO_URL="https://sso.example.com/userinfo"
|
||||
# AUTH_OAUTH_JWKS_URL="https://sso.example.com/jwks"
|
||||
# AUTH_OAUTH_TOKEN_AUTH_METHOD="client_secret_basic" # Allowed values: "client_secret_basic", "client_secret_post", "none"
|
||||
# AUTH_OAUTH_ENABLE_PKCE="true" # PKCE is enabled by default. Set to "false" to disable.
|
||||
|
||||
|
||||
@@ -48,3 +48,4 @@ next-env.d.ts
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
.env.docker
|
||||
|
||||
@@ -1,45 +1,68 @@
|
||||
# Docker Deployment Guide
|
||||
|
||||
Run your own prompts.chat instance with a single command.
|
||||
Run your own prompts.chat instance using Docker Compose.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name prompts \
|
||||
-p 4444:3000 \
|
||||
-v prompts-data:/data \
|
||||
ghcr.io/f/prompts.chat
|
||||
git clone https://github.com/f/prompts.chat.git
|
||||
cd prompts.chat
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**First run:** The container will clone the repository and build the app (~3-5 minutes).
|
||||
**Subsequent runs:** Starts immediately using the cached build.
|
||||
|
||||
Open http://localhost:4444 in your browser.
|
||||
|
||||
## Custom Branding
|
||||
## Using a Pre-built Image
|
||||
|
||||
Customize your instance with environment variables:
|
||||
Edit `compose.yml` and replace the `build` block with the published image:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name my-prompts \
|
||||
-p 4444:3000 \
|
||||
-v prompts-data:/data \
|
||||
-e PCHAT_NAME="Acme Prompts" \
|
||||
-e PCHAT_DESCRIPTION="Our team's AI prompt library" \
|
||||
-e PCHAT_COLOR="#ff6600" \
|
||||
-e PCHAT_AUTH_PROVIDERS="github,google" \
|
||||
-e PCHAT_LOCALES="en,es,fr" \
|
||||
ghcr.io/f/prompts.chat
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/Dockerfile
|
||||
image: ghcr.io/f/prompts.chat:latest
|
||||
```
|
||||
|
||||
> **Note:** Branding is applied during the first build. To change branding later, delete the volume and re-run:
|
||||
> ```bash
|
||||
> docker rm -f my-prompts
|
||||
> docker volume rm prompts-data
|
||||
> docker run ... # with new env vars
|
||||
> ```
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Standalone (Bring Your Own Database)
|
||||
|
||||
If you already have a PostgreSQL instance, you can run just the app container:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t prompts.chat .
|
||||
docker run -d \
|
||||
--name prompts \
|
||||
-p 4444:3000 \
|
||||
-e DATABASE_URL="postgresql://user:pass@your-db-host:5432/prompts?schema=public" \
|
||||
-e AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
prompts.chat
|
||||
```
|
||||
|
||||
## Custom Branding
|
||||
|
||||
All branding is configured via `PCHAT_*` environment variables at runtime -- no rebuild needed.
|
||||
|
||||
```yaml
|
||||
# compose.yml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
# ... existing vars ...
|
||||
PCHAT_NAME: "Acme Prompts"
|
||||
PCHAT_DESCRIPTION: "Our team's AI prompt library"
|
||||
PCHAT_COLOR: "#ff6600"
|
||||
PCHAT_AUTH_PROVIDERS: "github,google"
|
||||
PCHAT_LOCALES: "en,es,fr"
|
||||
```
|
||||
|
||||
Then restart: `docker compose up -d`
|
||||
|
||||
## Configuration Variables
|
||||
|
||||
@@ -95,103 +118,102 @@ All variables are prefixed with `PCHAT_` to avoid conflicts.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `AUTH_SECRET` | Secret for authentication tokens | Auto-generated |
|
||||
| `PORT` | Internal container port | `3000` |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Internal DB |
|
||||
| `AUTH_SECRET` | Secret for authentication tokens | Auto-generated (set explicitly for production) |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Set in compose.yml |
|
||||
| `DIRECT_URL` | Direct PostgreSQL URL (bypasses poolers) | Same as DATABASE_URL |
|
||||
| `PORT` | Host port mapping | `4444` |
|
||||
|
||||
## Production Setup
|
||||
|
||||
For production, set `AUTH_SECRET` explicitly:
|
||||
For production, always set `AUTH_SECRET` explicitly:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name prompts \
|
||||
-p 4444:3000 \
|
||||
-v prompts-data:/data \
|
||||
-e AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
-e PCHAT_NAME="My Company Prompts" \
|
||||
ghcr.io/f/prompts.chat
|
||||
# Generate a secret
|
||||
export AUTH_SECRET=$(openssl rand -base64 32)
|
||||
|
||||
# Start with explicit secret
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Or add it to a `.env` file next to `compose.yml`:
|
||||
|
||||
```env
|
||||
AUTH_SECRET=your-secret-key-here
|
||||
```
|
||||
|
||||
### With OAuth Providers
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name prompts \
|
||||
-p 4444:3000 \
|
||||
-v prompts-data:/data \
|
||||
-e AUTH_SECRET="your-secret-key" \
|
||||
-e PCHAT_AUTH_PROVIDERS="github,google" \
|
||||
-e AUTH_GITHUB_ID="your-github-client-id" \
|
||||
-e AUTH_GITHUB_SECRET="your-github-client-secret" \
|
||||
-e AUTH_GOOGLE_ID="your-google-client-id" \
|
||||
-e AUTH_GOOGLE_SECRET="your-google-client-secret" \
|
||||
ghcr.io/f/prompts.chat
|
||||
```yaml
|
||||
# compose.yml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
# ... existing vars ...
|
||||
PCHAT_AUTH_PROVIDERS: "github,google"
|
||||
AUTH_GITHUB_ID: "your-github-client-id"
|
||||
AUTH_GITHUB_SECRET: "your-github-client-secret"
|
||||
AUTH_GOOGLE_ID: "your-google-client-id"
|
||||
AUTH_GOOGLE_SECRET: "your-google-client-secret"
|
||||
```
|
||||
|
||||
### With AI Features (OpenAI)
|
||||
|
||||
```yaml
|
||||
# compose.yml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
# ... existing vars ...
|
||||
PCHAT_FEATURE_AI_SEARCH: "true"
|
||||
OPENAI_API_KEY: "sk-..."
|
||||
```
|
||||
|
||||
## Database Seeding
|
||||
|
||||
Seed the database with example prompts:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name prompts \
|
||||
-p 4444:3000 \
|
||||
-v prompts-data:/data \
|
||||
-e PCHAT_FEATURE_AI_SEARCH="true" \
|
||||
-e OPENAI_API_KEY="sk-..." \
|
||||
ghcr.io/f/prompts.chat
|
||||
docker compose exec app npx prisma db seed
|
||||
```
|
||||
|
||||
## Custom Logo
|
||||
|
||||
Mount your logo file:
|
||||
Mount your logo file into the app container:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name prompts \
|
||||
-p 4444:3000 \
|
||||
-v prompts-data:/data \
|
||||
-v ./my-logo.svg:/data/app/public/logo.svg \
|
||||
-e PCHAT_NAME="My App" \
|
||||
ghcr.io/f/prompts.chat
|
||||
```yaml
|
||||
# compose.yml
|
||||
services:
|
||||
app:
|
||||
volumes:
|
||||
- ./my-logo.svg:/app/public/logo.svg
|
||||
environment:
|
||||
PCHAT_LOGO: "/logo.svg"
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
### All-in-One Image
|
||||
|
||||
Data is stored in `/data` inside the container:
|
||||
- `/data/postgres` - PostgreSQL database files
|
||||
|
||||
Mount a volume to persist data:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-v prompts-data:/data \
|
||||
ghcr.io/f/prompts.chat
|
||||
```
|
||||
PostgreSQL data is stored in the `postgres_data` named volume and persists across container restarts, rebuilds, and image updates.
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
# Backup database
|
||||
docker exec prompts pg_dump -U prompts prompts > backup.sql
|
||||
docker compose exec db pg_dump -U prompts prompts > backup.sql
|
||||
|
||||
# Restore database
|
||||
docker exec -i prompts psql -U prompts prompts < backup.sql
|
||||
docker compose exec -T db psql -U prompts prompts < backup.sql
|
||||
```
|
||||
|
||||
## Building Locally
|
||||
|
||||
Build and run locally:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t prompts.chat .
|
||||
docker run -p 4444:3000 -v prompts-data:/data prompts.chat
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
The container includes a health check endpoint:
|
||||
The app container includes a health check endpoint:
|
||||
|
||||
```bash
|
||||
curl http://localhost:4444/api/health
|
||||
@@ -211,88 +233,108 @@ Response:
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# All logs
|
||||
docker logs prompts
|
||||
# All services
|
||||
docker compose logs
|
||||
|
||||
# Follow logs
|
||||
docker logs -f prompts
|
||||
docker compose logs -f
|
||||
|
||||
# PostgreSQL logs (inside container)
|
||||
docker exec prompts cat /var/log/supervisor/postgresql.log
|
||||
# App logs only
|
||||
docker compose logs app
|
||||
|
||||
# Next.js logs (inside container)
|
||||
docker exec prompts cat /var/log/supervisor/nextjs.log
|
||||
# Database logs only
|
||||
docker compose logs db
|
||||
```
|
||||
|
||||
### Database Access
|
||||
|
||||
```bash
|
||||
# Connect to PostgreSQL
|
||||
docker exec -it prompts psql -U prompts -d prompts
|
||||
docker compose exec db psql -U prompts -d prompts
|
||||
|
||||
# Run SQL query
|
||||
docker exec prompts psql -U prompts -d prompts -c "SELECT COUNT(*) FROM \"Prompt\""
|
||||
# Run a query
|
||||
docker compose exec db psql -U prompts -d prompts -c "SELECT COUNT(*) FROM \"Prompt\""
|
||||
```
|
||||
|
||||
### Container Shell
|
||||
|
||||
```bash
|
||||
docker exec -it prompts bash
|
||||
docker compose exec app sh
|
||||
docker compose exec db bash
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Container won't start:**
|
||||
- Check logs: `docker logs prompts`
|
||||
- Ensure port 4444 is available: `lsof -i :4444`
|
||||
**App container keeps restarting:**
|
||||
- Check logs: `docker compose logs app`
|
||||
- Database may not be ready yet -- the entrypoint retries for up to 60 seconds
|
||||
|
||||
**Database connection errors:**
|
||||
- Wait for PostgreSQL to initialize (can take 30-60 seconds on first run)
|
||||
- Check database logs: `docker exec prompts cat /var/log/supervisor/postgresql.log`
|
||||
- Verify the `db` service is healthy: `docker compose ps`
|
||||
- Check database logs: `docker compose logs db`
|
||||
|
||||
**Authentication issues:**
|
||||
- Ensure `AUTH_SECRET` is set for production
|
||||
- For OAuth, verify callback URLs are configured correctly
|
||||
|
||||
## Resource Requirements
|
||||
|
||||
Minimum:
|
||||
- 1 CPU core
|
||||
- 1GB RAM
|
||||
- 2GB disk space
|
||||
|
||||
Recommended:
|
||||
- 2 CPU cores
|
||||
- 2GB RAM
|
||||
- 10GB disk space
|
||||
- Set `AUTH_SECRET` explicitly for production
|
||||
- For OAuth, verify callback URLs match your domain
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
# Pull latest image
|
||||
docker pull ghcr.io/f/prompts.chat
|
||||
# If using pre-built images
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
# Stop and remove old container
|
||||
docker stop prompts && docker rm prompts
|
||||
|
||||
# Start new container (data persists in volume)
|
||||
docker run -d \
|
||||
--name prompts \
|
||||
-p 4444:3000 \
|
||||
-v prompts-data:/data \
|
||||
-e AUTH_SECRET="your-secret-key" \
|
||||
ghcr.io/f/prompts.chat
|
||||
# If building locally
|
||||
git pull
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
Data persists in the `postgres_data` volume across updates.
|
||||
|
||||
1. **Always set AUTH_SECRET** in production
|
||||
2. **Use HTTPS** - put a reverse proxy (nginx, Caddy, Traefik) in front
|
||||
3. **Limit exposed ports** - only expose what's needed
|
||||
4. **Regular updates** - pull the latest image regularly
|
||||
5. **Backup data** - regularly backup the `/data` volume
|
||||
## Migrating from the Old Single-Image Setup
|
||||
|
||||
## Example: Running Behind Nginx
|
||||
If you were using the previous all-in-one Docker image:
|
||||
|
||||
```bash
|
||||
# 1. Export your database from the old container
|
||||
docker exec prompts pg_dump -U prompts prompts > backup.sql
|
||||
|
||||
# 2. Stop and remove the old container
|
||||
docker stop prompts && docker rm prompts
|
||||
|
||||
# 3. Start the new compose setup
|
||||
docker compose up -d
|
||||
|
||||
# 4. Import your data
|
||||
docker compose exec -T db psql -U prompts prompts < backup.sql
|
||||
```
|
||||
|
||||
## Resource Requirements
|
||||
|
||||
**Runtime** (after first build):
|
||||
- 1 CPU core
|
||||
- 1GB RAM
|
||||
- 2GB disk space
|
||||
|
||||
**First-run build** (Next.js compilation on startup):
|
||||
- 1 CPU core
|
||||
- Higher memory required (OOM may occur with low limits)
|
||||
- 2GB disk space
|
||||
|
||||
> ⚠️ If you see `Killed` followed by `exited with code 137` during first startup,
|
||||
> your Docker container likely ran out of memory during the build step.
|
||||
> Increasing Docker's memory allocation (e.g., ~4GB or more) can help resolve this.
|
||||
> On Docker Desktop: Settings → Resources → Memory.
|
||||
|
||||
**Recommended for production:**
|
||||
- 2 CPU cores
|
||||
- 2GB RAM (runtime)
|
||||
- 10GB disk space
|
||||
|
||||
## Running Behind a Reverse Proxy
|
||||
|
||||
### Nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -316,6 +358,23 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
### Caddy
|
||||
|
||||
```caddyfile
|
||||
prompts.example.com {
|
||||
reverse_proxy localhost:4444
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Always set AUTH_SECRET** in production
|
||||
2. **Use HTTPS** -- put a reverse proxy (Nginx, Caddy, Traefik) in front
|
||||
3. **Change default database password** -- update `POSTGRES_PASSWORD` in compose.yml and the connection strings
|
||||
4. **Limit exposed ports** -- only expose what's needed
|
||||
5. **Regular updates** -- pull the latest image regularly
|
||||
6. **Backup data** -- regularly backup the database
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Governance
|
||||
|
||||
This document describes the governance model for prompts.chat.
|
||||
|
||||
## Overview
|
||||
|
||||
prompts.chat is an open-source project that welcomes contributions from anyone. The project operates with a small group of maintainers who are responsible for the overall direction, code review, and release process. We aim to grow this group over time and welcome new maintainers who demonstrate sustained, quality contributions.
|
||||
|
||||
## Roles
|
||||
|
||||
### Users
|
||||
|
||||
Anyone who uses prompts.chat, whether by browsing the website, self-hosting the platform, using the CLI, or integrating with the MCP server. Users are encouraged to file issues, suggest features, and contribute prompts.
|
||||
|
||||
### Contributors
|
||||
|
||||
Anyone who contributes to the project. This includes submitting prompts through the web platform, opening pull requests, reporting bugs, improving documentation, or helping other users. All contributors are listed at https://github.com/f/prompts.chat/graphs/contributors.
|
||||
|
||||
### Maintainers
|
||||
|
||||
People with commit access to the repository. Maintainers review and merge pull requests, triage issues, and participate in project decisions. The current list of maintainers is in [OWNERS.md](OWNERS.md).
|
||||
|
||||
Maintainers are expected to:
|
||||
- Review pull requests in a timely manner
|
||||
- Help triage and respond to issues
|
||||
- Follow the project's code of conduct
|
||||
- Participate in governance discussions when they arise
|
||||
|
||||
### Project Lead
|
||||
|
||||
The project lead has final decision-making authority on project direction, releases, and governance changes. The project lead is responsible for ensuring the project stays aligned with its mission and the broader community's needs.
|
||||
|
||||
## Becoming a Maintainer
|
||||
|
||||
New maintainers are added based on sustained, high-quality contributions and demonstrated understanding of the project. There is no fixed checklist, but the following signals are considered:
|
||||
|
||||
- A track record of accepted pull requests over several months
|
||||
- Thoughtful code reviews on other people's contributions
|
||||
- Constructive participation in issue discussions
|
||||
- Reliability and follow-through on commitments
|
||||
|
||||
Any existing maintainer can nominate a contributor for maintainer status. The nomination is discussed among the current maintainers, and the project lead makes the final decision. We are actively looking to grow the maintainer team, especially from contributors at different organizations.
|
||||
|
||||
## Decision Making
|
||||
|
||||
Most day-to-day decisions (bug fixes, small features, documentation updates) are made through the standard pull request review process. Any maintainer can approve and merge a PR.
|
||||
|
||||
For larger decisions (new features with broad impact, architectural changes, governance changes, dependency additions), we use a "lazy consensus" approach:
|
||||
|
||||
1. A proposal is opened as a GitHub issue or discussion.
|
||||
2. The proposal is left open for at least 7 days to allow feedback.
|
||||
3. If there are no objections from maintainers, the proposal is accepted.
|
||||
4. If there are objections, the maintainers discuss and try to reach agreement. The project lead has final say if consensus cannot be reached.
|
||||
|
||||
## Changing This Document
|
||||
|
||||
Changes to this governance document follow the same decision-making process described above. Any maintainer or contributor can propose changes by opening a pull request.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
All participants in the project are expected to be respectful, constructive, and welcoming. We do not tolerate harassment, discrimination, or abusive behavior in any form. Violations can be reported to the project lead or through the channels listed in [SECURITY.md](SECURITY.md).
|
||||
@@ -1,121 +1,14 @@
|
||||
Creative Commons Legal Code
|
||||
This project uses a dual-license model:
|
||||
|
||||
CC0 1.0 Universal
|
||||
- **Source code and site-authored content** (everything under src/,
|
||||
prisma/, scripts/, and all configuration/build files) is licensed under
|
||||
the MIT License. This includes the interactive book content in
|
||||
src/content/book/.
|
||||
See LICENSE-MIT for the full text.
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
- **Prompt content and data** (prompts.csv, PROMPTS.md, and all user-submitted
|
||||
prompt text) is dedicated to the public domain under CC0 1.0 Universal.
|
||||
See LICENSE-CC0 for the full text.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
If you are unsure which license applies to a particular file, the MIT License
|
||||
applies by default.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-present Fatih Kadir Akin and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Owners
|
||||
|
||||
This file lists the current maintainers and emeritus maintainers of prompts.chat.
|
||||
|
||||
## Project Lead
|
||||
|
||||
| Name | GitHub | Affiliation |
|
||||
|------|--------|-------------|
|
||||
| Fatih Kadir Akin | [@f](https://github.com/f) | Independent |
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Name | GitHub | Affiliation | Since |
|
||||
|------|--------|-------------|-------|
|
||||
| Fatih Kadir Akin | [@f](https://github.com/f) | Independent | Dec 2022 |
|
||||
|
||||
## Emeritus Maintainers
|
||||
|
||||
None yet.
|
||||
|
||||
---
|
||||
|
||||
We are actively looking to grow the maintainer team. If you are interested in becoming a maintainer, please see [GOVERNANCE.md](GOVERNANCE.md) for how the process works. Sustained contributions and involvement in code review are the best way to get started.
|
||||
@@ -34,12 +34,12 @@
|
||||
|
||||
<p align="center">
|
||||
<sub>
|
||||
🏆 Featured in <a href="https://www.forbes.com/sites/tjmccue/2023/01/19/chatgpt-success-completely-depends-on-your-prompt/">Forbes</a> ·
|
||||
🎓 Referenced by <a href="https://www.huit.harvard.edu/news/ai-prompts">Harvard</a>, <a href="https://etc.cuit.columbia.edu/news/columbia-prompt-library-effective-academic-ai-use">Columbia</a> ·
|
||||
📄 <a href="https://scholar.google.com/citations?user=AZ0Dg8YAAAAJ&hl=en">40+ academic citations</a> ·
|
||||
🏆 Featured in <a href="https://www.forbes.com/sites/tjmccue/2023/01/19/chatgpt-success-completely-depends-on-your-prompt/">Forbes</a> ·
|
||||
🎓 Referenced by <a href="https://www.huit.harvard.edu/news/ai-prompts">Harvard</a>, <a href="https://etc.cuit.columbia.edu/news/columbia-prompt-library-effective-academic-ai-use">Columbia</a> ·
|
||||
📄 <a href="https://scholar.google.com/citations?user=AZ0Dg8YAAAAJ&hl=en">40+ academic citations</a> ·
|
||||
❤️ <a href="https://huggingface.co/datasets/fka/prompts.chat">Most liked dataset</a> on Hugging Face<br>
|
||||
⭐ 143k+ GitHub stars ·
|
||||
🏅 <a href="https://spotlights-feed.github.com/spotlights/prompts-chat/index/">GitHub Staff Pick</a> ·
|
||||
⭐ 143k+ GitHub stars ·
|
||||
🏅 <a href="https://spotlights-feed.github.com/spotlights/prompts-chat/index/">GitHub Staff Pick</a> ·
|
||||
🚀 First prompt library (Dec 2022)
|
||||
</sub>
|
||||
</p>
|
||||
@@ -47,9 +47,9 @@
|
||||
<p align="center">
|
||||
<sub><strong>Loved by AI pioneers:</strong></sub><br>
|
||||
<sub>
|
||||
<a href="https://x.com/gdb/status/1602072566671110144"><strong>Greg Brockman</strong></a> (OpenAI Co-Founder) ·
|
||||
<a href="https://x.com/woj_zaremba/status/1601362952841760769"><strong>Wojciech Zaremba</strong></a> (OpenAI Co-Founder) ·
|
||||
<a href="https://x.com/clementdelangue/status/1830976369389642059"><strong>Clement Delangue</strong></a> (Hugging Face CEO) ·
|
||||
<a href="https://x.com/gdb/status/1602072566671110144"><strong>Greg Brockman</strong></a> (OpenAI Co-Founder) ·
|
||||
<a href="https://x.com/woj_zaremba/status/1601362952841760769"><strong>Wojciech Zaremba</strong></a> (OpenAI Co-Founder) ·
|
||||
<a href="https://x.com/clementdelangue/status/1830976369389642059"><strong>Clement Delangue</strong></a> (Hugging Face CEO) ·
|
||||
<a href="https://x.com/ashtom/status/1887250944427237816"><strong>Thomas Dohmke</strong></a> (Former GitHub CEO)
|
||||
</sub>
|
||||
</p>
|
||||
@@ -73,7 +73,7 @@ A curated collection of **prompt examples** for AI chat models. Originally creat
|
||||
|
||||
Learn prompt engineering with our **free, interactive guide** — 25+ chapters covering everything from basics to advanced techniques like chain-of-thought reasoning, few-shot learning, and AI agents.
|
||||
|
||||
**[Start Reading →](https://fka.gumroad.com/l/art-of-chatgpt-prompting)**
|
||||
**[Start Reading →](https://fka.gumroad.com/l/art-of-chatgpt-prompting)** (Source: https://github.com/f/prompts.chat/tree/main/src/content/book)
|
||||
|
||||
---
|
||||
|
||||
@@ -238,4 +238,10 @@ Use prompts.chat as an MCP server in your AI tools.
|
||||
|
||||
## 📜 License
|
||||
|
||||
**[CC0 1.0 Universal (Public Domain)](https://creativecommons.org/publicdomain/zero/1.0/)** — Copy, modify, distribute, and use freely. No attribution required.
|
||||
This project is dual-licensed:
|
||||
|
||||
- **Source code and site-authored content** is licensed under the [MIT License](LICENSE-MIT),
|
||||
including the interactive book content in [`src/content/book`](src/content/book).
|
||||
- **Prompt content and data** (prompts.csv, PROMPTS.md, user-submitted prompts) is dedicated to the public domain under [CC0 1.0 Universal](LICENSE-CC0).
|
||||
|
||||
See [LICENSE](LICENSE) for details.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| Latest | Yes |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in prompts.chat, please report it responsibly.
|
||||
|
||||
**Do NOT open a public GitHub issue for security vulnerabilities.**
|
||||
|
||||
Instead, please report vulnerabilities by emailing **security@prompts.chat**.
|
||||
|
||||
Include the following in your report:
|
||||
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if any)
|
||||
|
||||
## CVE Coordination
|
||||
|
||||
We coordinate the CVE identification and disclosure process with the GitHub Security team. Confirmed
|
||||
vulnerabilities will be tracked through GitHub Security Advisories, and CVE IDs will be requested
|
||||
and assigned as appropriate.
|
||||
|
||||
## Scope
|
||||
The following are **out of scope**:
|
||||
|
||||
- Denial of service attacks
|
||||
- Social engineering
|
||||
- Issues in third-party dependencies (report these upstream)
|
||||
- Attacks requiring physical access
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
We ask that you give us reasonable time to address the issue before any public disclosure. We are
|
||||
committed to working with security researchers and will credit reporters (unless anonymity is
|
||||
preferred) once the issue is resolved.
|
||||
|
||||
## Thank You
|
||||
|
||||
We appreciate the security research community's efforts in helping keep prompts.chat and its users
|
||||
safe.
|
||||
@@ -32,9 +32,9 @@ This guide explains how to deploy **prompts.chat** on your own private server fo
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** 18+
|
||||
- **Node.js** 24.x
|
||||
- **PostgreSQL** database
|
||||
- **npm** or **yarn**
|
||||
- **npm**
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:17-bookworm
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: prompts
|
||||
# Change this for production — see Security Considerations in DOCKER.md
|
||||
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-prompts}"
|
||||
POSTGRES_DB: prompts
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U prompts -d prompts"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
app:
|
||||
image: ghcr.io/f/prompts.chat:latest
|
||||
# To build locally instead of using the pre-built image, comment out
|
||||
# the 'image' line above and uncomment the build block below:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-4444}:3000"
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://prompts:${POSTGRES_PASSWORD:-prompts}@db:5432/prompts?schema=public"
|
||||
DIRECT_URL: "postgresql://prompts:${POSTGRES_PASSWORD:-prompts}@db:5432/prompts?schema=public"
|
||||
AUTH_SECRET: "${AUTH_SECRET:-}"
|
||||
NODE_ENV: production
|
||||
# Branding (all optional — set any PCHAT_* variable to customize at runtime)
|
||||
# PCHAT_NAME: "My Prompt Library"
|
||||
# PCHAT_DESCRIPTION: "Our team's AI prompt collection"
|
||||
# PCHAT_COLOR: "#6366f1"
|
||||
# PCHAT_AUTH_PROVIDERS: "credentials"
|
||||
# PCHAT_LOCALES: "en"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -1,49 +1,94 @@
|
||||
# prompts.chat Bootstrap Docker Image
|
||||
# Lightweight image that clones and builds on first run
|
||||
# prompts.chat Docker Image
|
||||
# Multi-stage build for a production-ready Next.js application
|
||||
#
|
||||
# Usage:
|
||||
# docker run -p 4444:3000 -v prompts-data:/data ghcr.io/f/prompts.chat
|
||||
# docker run -p 4444:3000 -v prompts-data:/data -e PCHAT_NAME="My App" ghcr.io/f/prompts.chat
|
||||
# Usage with Docker Compose:
|
||||
# docker compose up -d
|
||||
#
|
||||
# Usage standalone (bring your own PostgreSQL):
|
||||
# docker build -f docker/Dockerfile -t prompts.chat .
|
||||
# docker run -p 4444:3000 -e DATABASE_URL="postgresql://..." prompts.chat
|
||||
|
||||
FROM node:24-bookworm-slim
|
||||
# ---- Stage 1: Base ----
|
||||
FROM node:24-bookworm-slim AS base
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openssl \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ---- Stage 2: Dependencies ----
|
||||
FROM base AS deps
|
||||
|
||||
# Copy dependency manifests and Prisma schema (postinstall runs prisma generate)
|
||||
COPY package.json package-lock.json ./
|
||||
COPY prisma/schema.prisma prisma/schema.prisma
|
||||
COPY prisma.config.ts tsconfig.json ./
|
||||
|
||||
# Dummy DATABASE_URL for prisma generate (no actual connection is made)
|
||||
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
|
||||
|
||||
RUN npm ci
|
||||
|
||||
# ---- Stage 3: Builder ----
|
||||
FROM base AS builder
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# ---- Stage 4: Runner ----
|
||||
FROM node:24-bookworm-slim AS runner
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/f/prompts.chat"
|
||||
LABEL org.opencontainers.image.description="prompts.chat - Self-hosted AI prompt library"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
|
||||
# Install PostgreSQL, git, and utilities
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
postgresql-15 \
|
||||
postgresql-contrib-15 \
|
||||
supervisor \
|
||||
git \
|
||||
curl \
|
||||
openssl \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /var/run/postgresql /var/log/supervisor \
|
||||
&& chown -R postgres:postgres /var/run/postgresql
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create directories
|
||||
WORKDIR /app
|
||||
RUN mkdir -p /data/postgres /data/app && chown -R postgres:postgres /data/postgres
|
||||
|
||||
# Copy bootstrap scripts
|
||||
COPY docker/bootstrap.sh /bootstrap.sh
|
||||
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
RUN chmod +x /bootstrap.sh
|
||||
RUN groupadd --system --gid 1001 nodejs && \
|
||||
useradd --system --uid 1001 --gid nodejs nextjs
|
||||
|
||||
# Copy standalone build output
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
|
||||
# Copy Prisma schema, migrations, and config for runtime migrations
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./prisma.config.ts
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/tsconfig.json ./tsconfig.json
|
||||
|
||||
# Install only Prisma CLI and its dependencies for runtime migrations
|
||||
RUN npm install --no-save prisma@6.19 dotenv
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY --chown=nextjs:nodejs docker/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
# Environment defaults
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
ENV DATABASE_URL="postgresql://prompts:prompts@localhost:5432/prompts?schema=public"
|
||||
ENV DIRECT_URL="postgresql://prompts:prompts@localhost:5432/prompts?schema=public"
|
||||
ENV REPO_URL="https://github.com/f/prompts.chat.git"
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=3 \
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://localhost:3000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/bootstrap.sh"]
|
||||
USER nextjs
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ 🚀 prompts.chat - AI Prompt Library ║"
|
||||
echo "║ ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Paths
|
||||
APP_DIR="/data/app"
|
||||
PGDATA="/data/postgres"
|
||||
PGBIN="/usr/lib/postgresql/15/bin"
|
||||
BUILD_MARKER="/data/.built"
|
||||
|
||||
# Generate AUTH_SECRET if not provided
|
||||
if [ -z "$AUTH_SECRET" ]; then
|
||||
if [ -f "/data/.auth_secret" ]; then
|
||||
export AUTH_SECRET=$(cat /data/.auth_secret)
|
||||
else
|
||||
export AUTH_SECRET=$(openssl rand -base64 32)
|
||||
echo "$AUTH_SECRET" > /data/.auth_secret
|
||||
echo "⚠ AUTH_SECRET generated and saved"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize PostgreSQL if needed
|
||||
if [ ! -f "$PGDATA/PG_VERSION" ]; then
|
||||
echo "▶ Initializing PostgreSQL..."
|
||||
su postgres -c "$PGBIN/initdb -D $PGDATA"
|
||||
|
||||
cat >> "$PGDATA/postgresql.conf" << EOF
|
||||
listen_addresses = 'localhost'
|
||||
port = 5432
|
||||
max_connections = 100
|
||||
shared_buffers = 128MB
|
||||
EOF
|
||||
|
||||
cat > "$PGDATA/pg_hba.conf" << EOF
|
||||
local all all trust
|
||||
host all all 127.0.0.1/32 trust
|
||||
host all all ::1/128 trust
|
||||
EOF
|
||||
|
||||
su postgres -c "$PGBIN/pg_ctl -D $PGDATA -l /tmp/pg.log start"
|
||||
sleep 3
|
||||
su postgres -c "$PGBIN/createuser -s prompts 2>/dev/null" || true
|
||||
su postgres -c "$PGBIN/createdb -O prompts prompts 2>/dev/null" || true
|
||||
su postgres -c "$PGBIN/pg_ctl -D $PGDATA stop"
|
||||
sleep 2
|
||||
echo "✓ PostgreSQL initialized"
|
||||
fi
|
||||
|
||||
# Clone and build on first run
|
||||
if [ ! -f "$BUILD_MARKER" ]; then
|
||||
echo ""
|
||||
echo "▶ First run detected - building prompts.chat..."
|
||||
echo ""
|
||||
|
||||
# Clone repository
|
||||
if [ ! -d "$APP_DIR/.git" ]; then
|
||||
echo "▶ Cloning repository..."
|
||||
rm -rf "$APP_DIR"
|
||||
git clone --depth 1 "$REPO_URL" "$APP_DIR"
|
||||
echo "✓ Repository cloned"
|
||||
fi
|
||||
|
||||
cd "$APP_DIR"
|
||||
|
||||
# Clean up unnecessary files
|
||||
rm -rf .github .claude packages .git
|
||||
|
||||
# Install dependencies (including devDependencies needed for build)
|
||||
echo "▶ Installing dependencies..."
|
||||
NODE_ENV=development npm ci
|
||||
echo "✓ Dependencies installed"
|
||||
|
||||
# Run docker-setup.js to generate config with branding
|
||||
echo "▶ Generating configuration..."
|
||||
node scripts/docker-setup.js
|
||||
echo "✓ Configuration generated"
|
||||
|
||||
# Generate Prisma client
|
||||
echo "▶ Generating Prisma client..."
|
||||
npx prisma generate
|
||||
echo "✓ Prisma client generated"
|
||||
|
||||
# Build Next.js
|
||||
echo "▶ Building Next.js application (this may take a few minutes)..."
|
||||
npm run build
|
||||
echo "✓ Build complete"
|
||||
|
||||
# Copy static files for standalone mode
|
||||
echo "▶ Copying static assets..."
|
||||
cp -r .next/static .next/standalone/.next/
|
||||
cp -r public .next/standalone/
|
||||
echo "✓ Static assets copied"
|
||||
|
||||
# Mark as built
|
||||
touch "$BUILD_MARKER"
|
||||
|
||||
echo ""
|
||||
echo "✅ Build complete! Starting application..."
|
||||
echo ""
|
||||
else
|
||||
echo "✓ Using existing build"
|
||||
cd "$APP_DIR"
|
||||
fi
|
||||
|
||||
# Start supervisord (manages PostgreSQL and Next.js)
|
||||
echo "▶ Starting services..."
|
||||
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf &
|
||||
SUPERVISOR_PID=$!
|
||||
|
||||
# Wait for PostgreSQL
|
||||
echo "▶ Waiting for PostgreSQL..."
|
||||
for i in $(seq 1 30); do
|
||||
if $PGBIN/pg_isready -h localhost -p 5432 >/dev/null 2>&1; then
|
||||
echo "✓ PostgreSQL is ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "✗ PostgreSQL failed to start"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Run migrations
|
||||
echo "▶ Running database migrations..."
|
||||
cd "$APP_DIR"
|
||||
npx prisma migrate deploy
|
||||
echo "✓ Migrations complete"
|
||||
|
||||
# Seed on first run only
|
||||
SEED_MARKER="/data/.seeded"
|
||||
if [ ! -f "$SEED_MARKER" ]; then
|
||||
echo "▶ Seeding database..."
|
||||
if npx tsx prisma/seed.ts 2>/dev/null; then
|
||||
touch "$SEED_MARKER"
|
||||
echo "✓ Database seeded"
|
||||
else
|
||||
echo "⚠ Seeding skipped"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Wait for supervisord socket to be ready
|
||||
echo "▶ Waiting for supervisord..."
|
||||
for i in $(seq 1 30); do
|
||||
if supervisorctl -c /etc/supervisor/conf.d/supervisord.conf status >/dev/null 2>&1; then
|
||||
echo "✓ Supervisord is ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "✗ Supervisord failed to start"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Start Next.js
|
||||
echo "▶ Starting Next.js..."
|
||||
supervisorctl -c /etc/supervisor/conf.d/supervisord.conf start nextjs
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ ✅ prompts.chat is running! ║"
|
||||
echo "║ ║"
|
||||
echo "║ 🌐 Open http://localhost:${PORT:-80} in your browser ║"
|
||||
echo "║ ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
wait $SUPERVISOR_PID
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Generate AUTH_SECRET if not provided
|
||||
if [ -z "$AUTH_SECRET" ]; then
|
||||
AUTH_SECRET=$(openssl rand -base64 32)
|
||||
export AUTH_SECRET
|
||||
echo "[prompts.chat] AUTH_SECRET not set -- generated a random value."
|
||||
echo "[prompts.chat] Set AUTH_SECRET explicitly for production to persist sessions across restarts."
|
||||
fi
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "[prompts.chat] Waiting for database..."
|
||||
MAX_RETRIES=30
|
||||
RETRY_COUNT=0
|
||||
until node -e "
|
||||
const net = require('net');
|
||||
const url = new URL(process.env.DATABASE_URL);
|
||||
const sock = net.createConnection({ host: url.hostname, port: url.port || 5432 });
|
||||
sock.setTimeout(2000);
|
||||
sock.on('connect', () => { sock.setTimeout(0); sock.destroy(); process.exit(0); });
|
||||
sock.on('timeout', () => { sock.destroy(); process.exit(1); });
|
||||
sock.on('error', () => process.exit(1));
|
||||
" 2>/dev/null; do
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
if [ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]; then
|
||||
echo "[prompts.chat] ERROR: Database not reachable after ${MAX_RETRIES} attempts."
|
||||
exit 1
|
||||
fi
|
||||
echo "[prompts.chat] Database not ready (attempt ${RETRY_COUNT}/${MAX_RETRIES}), retrying in 2s..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Run database migrations
|
||||
echo "[prompts.chat] Running database migrations..."
|
||||
npx prisma migrate deploy
|
||||
echo "[prompts.chat] Migrations applied successfully."
|
||||
|
||||
# Start the application
|
||||
echo "[prompts.chat] Starting application on port ${PORT:-3000}..."
|
||||
exec node server.js
|
||||
@@ -1,28 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
childlogdir=/var/log/supervisor
|
||||
|
||||
[program:postgresql]
|
||||
command=/usr/lib/postgresql/15/bin/postgres -D /data/postgres
|
||||
user=postgres
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_logfile=/var/log/supervisor/postgresql.log
|
||||
stderr_logfile=/var/log/supervisor/postgresql-error.log
|
||||
|
||||
[program:nextjs]
|
||||
command=node /data/app/.next/standalone/server.js
|
||||
directory=/data/app
|
||||
user=root
|
||||
autostart=false
|
||||
autorestart=true
|
||||
priority=20
|
||||
startsecs=10
|
||||
startretries=3
|
||||
environment=NODE_ENV="production",PORT="%(ENV_PORT)s",HOSTNAME="0.0.0.0",DATABASE_URL="%(ENV_DATABASE_URL)s",AUTH_SECRET="%(ENV_AUTH_SECRET)s"
|
||||
stdout_logfile=/var/log/supervisor/nextjs.log
|
||||
stderr_logfile=/var/log/supervisor/nextjs-error.log
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "أو استكشف حسب المجال",
|
||||
"clickToExplore": "انقر للاستكشاف",
|
||||
"searchPlaceholder": "ابحث عن برومبتات...",
|
||||
"teachers": "المعلمون",
|
||||
"developers": "المطورون",
|
||||
"marketers": "المسوقون",
|
||||
"designers": "المصممون",
|
||||
"writers": "الكتّاب",
|
||||
"analysts": "المحللون",
|
||||
"entrepreneurs": "رواد الأعمال",
|
||||
"researchers": "الباحثون",
|
||||
"students": "الطلاب",
|
||||
"consultants": "المستشارون",
|
||||
"engineers": "المهندسون",
|
||||
"creators": "المبدعون",
|
||||
"lawyers": "المحامون",
|
||||
"doctors": "الأطباء",
|
||||
"nurses": "الممرضون",
|
||||
"accountants": "المحاسبون",
|
||||
"salespeople": "المبيعات",
|
||||
"recruiters": "التوظيف",
|
||||
"managers": "المدراء",
|
||||
"executives": "التنفيذيون",
|
||||
"freelancers": "المستقلون",
|
||||
"photographers": "المصورون",
|
||||
"musicians": "الموسيقيون",
|
||||
"artists": "الفنانون",
|
||||
"architects": "المعماريون",
|
||||
"scientists": "العلماء",
|
||||
"journalists": "الصحفيون",
|
||||
"editors": "المحررون",
|
||||
"translators": "المترجمون",
|
||||
"coaches": "المدربون",
|
||||
"therapists": "المعالجون",
|
||||
"trainers": "المدربون",
|
||||
"chefs": "الطهاة",
|
||||
"realtors": "العقاريون",
|
||||
"investors": "المستثمرون",
|
||||
"traders": "المتداولون"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "الصفحة غير موجودة",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "ادعم الآن",
|
||||
"techStackTitle": "تطور التقنية",
|
||||
"coreContributorsTitle": "المساهمون الأساسيون",
|
||||
"securityTitle": "الأمان",
|
||||
"designCreditsTitle": "التصميم",
|
||||
"ideationTitle": "الأفكار",
|
||||
"communityContributorsTitle": "مساهمو المجتمع",
|
||||
@@ -2263,5 +2227,12 @@
|
||||
"submit": "إرسال",
|
||||
"communityExamples": "أمثلة المجتمع",
|
||||
"userExample": "مثال المستخدم"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,45 +1025,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "və ya sahəyə görə kəşf edin",
|
||||
"clickToExplore": "Kəşf etmək üçün klikləyin",
|
||||
"searchPlaceholder": "Promptları axtar...",
|
||||
"teachers": "Müəllimlər",
|
||||
"developers": "Tərtibatçılar",
|
||||
"marketers": "Marketoloqlar",
|
||||
"designers": "Dizaynerlər",
|
||||
"writers": "Yazıçılar",
|
||||
"analysts": "Analitiklər",
|
||||
"entrepreneurs": "Sahibkarlar",
|
||||
"researchers": "Tədqiqatçılar",
|
||||
"students": "Tələbələr",
|
||||
"consultants": "Məsləhətçilər",
|
||||
"engineers": "Mühəndislər",
|
||||
"creators": "Yaradıcılar",
|
||||
"lawyers": "Vəkillər",
|
||||
"doctors": "Həkimlər",
|
||||
"nurses": "Tibb bacıları",
|
||||
"accountants": "Mühasiblər",
|
||||
"salespeople": "Satıcılar",
|
||||
"recruiters": "İşə götürənlər",
|
||||
"managers": "Menecerlər",
|
||||
"executives": "İcraçılar",
|
||||
"freelancers": "Frilanslər",
|
||||
"photographers": "Fotoqraflar",
|
||||
"musicians": "Musiqiçilər",
|
||||
"artists": "Rəssamlar",
|
||||
"architects": "Memarlar",
|
||||
"scientists": "Alimlər",
|
||||
"journalists": "Jurnalistlər",
|
||||
"editors": "Redaktorlar",
|
||||
"translators": "Tərcüməçilər",
|
||||
"coaches": "Məşqçilər",
|
||||
"therapists": "Terapevtlər",
|
||||
"trainers": "Təlimçilər",
|
||||
"chefs": "Aşpazlar",
|
||||
"realtors": "Rieltorlar",
|
||||
"investors": "İnvestorlar",
|
||||
"traders": "Treyderlər"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"promptBuilder": {
|
||||
"title": "Prompt Qurma Agenti",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "İndi Dəstəklə",
|
||||
"techStackTitle": "Texnoloji Təkamül",
|
||||
"coreContributorsTitle": "Əsas Töhfəçilər",
|
||||
"securityTitle": "Təhlükəsizlik",
|
||||
"designCreditsTitle": "Dizayn",
|
||||
"ideationTitle": "İdeya",
|
||||
"communityContributorsTitle": "İcma Töhfəçiləri",
|
||||
@@ -2277,5 +2241,12 @@
|
||||
"submit": "Göndər",
|
||||
"communityExamples": "İcma Nümunələri",
|
||||
"userExample": "İstifadəçi nümunəsi"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "oder nach Branche erkunden",
|
||||
"clickToExplore": "Klicke zum Erkunden",
|
||||
"searchPlaceholder": "Prompts suchen...",
|
||||
"teachers": "Lehrer",
|
||||
"developers": "Entwickler",
|
||||
"marketers": "Marketer",
|
||||
"designers": "Designer",
|
||||
"writers": "Autoren",
|
||||
"analysts": "Analysten",
|
||||
"entrepreneurs": "Unternehmer",
|
||||
"researchers": "Forscher",
|
||||
"students": "Studenten",
|
||||
"consultants": "Berater",
|
||||
"engineers": "Ingenieure",
|
||||
"creators": "Creator",
|
||||
"lawyers": "Anwälte",
|
||||
"doctors": "Ärzte",
|
||||
"nurses": "Krankenpfleger",
|
||||
"accountants": "Buchhalter",
|
||||
"salespeople": "Verkäufer",
|
||||
"recruiters": "Recruiter",
|
||||
"managers": "Manager",
|
||||
"executives": "Führungskräfte",
|
||||
"freelancers": "Freelancer",
|
||||
"photographers": "Fotografen",
|
||||
"musicians": "Musiker",
|
||||
"artists": "Künstler",
|
||||
"architects": "Architekten",
|
||||
"scientists": "Wissenschaftler",
|
||||
"journalists": "Journalisten",
|
||||
"editors": "Redakteure",
|
||||
"translators": "Übersetzer",
|
||||
"coaches": "Coaches",
|
||||
"therapists": "Therapeuten",
|
||||
"trainers": "Trainer",
|
||||
"chefs": "Köche",
|
||||
"realtors": "Makler",
|
||||
"investors": "Investoren",
|
||||
"traders": "Händler"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Seite nicht gefunden",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Jetzt unterstützen",
|
||||
"techStackTitle": "Technologie-Evolution",
|
||||
"coreContributorsTitle": "Kern-Mitwirkende",
|
||||
"securityTitle": "Sicherheit",
|
||||
"designCreditsTitle": "Design",
|
||||
"ideationTitle": "Ideenfindung",
|
||||
"communityContributorsTitle": "Community-Mitwirkende",
|
||||
@@ -2277,5 +2241,12 @@
|
||||
"submit": "Absenden",
|
||||
"communityExamples": "Community-Beispiele",
|
||||
"userExample": "Benutzerbeispiel"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1051,45 +1051,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "ή εξερευνήστε ανά κλάδο",
|
||||
"clickToExplore": "Κάντε κλικ για εξερεύνηση",
|
||||
"searchPlaceholder": "Αναζήτηση prompts...",
|
||||
"teachers": "Εκπαιδευτικούς",
|
||||
"developers": "Προγραμματιστές",
|
||||
"marketers": "Μάρκετερ",
|
||||
"designers": "Σχεδιαστές",
|
||||
"writers": "Συγγραφείς",
|
||||
"analysts": "Αναλυτές",
|
||||
"entrepreneurs": "Επιχειρηματίες",
|
||||
"researchers": "Ερευνητές",
|
||||
"students": "Φοιτητές",
|
||||
"consultants": "Συμβούλους",
|
||||
"engineers": "Μηχανικούς",
|
||||
"creators": "Δημιουργούς",
|
||||
"lawyers": "Δικηγόρους",
|
||||
"doctors": "Γιατρούς",
|
||||
"nurses": "Νοσηλευτές",
|
||||
"accountants": "Λογιστές",
|
||||
"salespeople": "Πωλητές",
|
||||
"recruiters": "Recruiters",
|
||||
"managers": "Διευθυντές",
|
||||
"executives": "Στελέχη",
|
||||
"freelancers": "Ελεύθερους",
|
||||
"photographers": "Φωτογράφους",
|
||||
"musicians": "Μουσικούς",
|
||||
"artists": "Καλλιτέχνες",
|
||||
"architects": "Αρχιτέκτονες",
|
||||
"scientists": "Επιστήμονες",
|
||||
"journalists": "Δημοσιογράφους",
|
||||
"editors": "Συντάκτες",
|
||||
"translators": "Μεταφραστές",
|
||||
"coaches": "Προπονητές",
|
||||
"therapists": "Θεραπευτές",
|
||||
"trainers": "Εκπαιδευτές",
|
||||
"chefs": "Σεφ",
|
||||
"realtors": "Μεσίτες",
|
||||
"investors": "Επενδυτές",
|
||||
"traders": "Traders"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"promptBuilder": {
|
||||
"title": "Prompt Building Agent",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Υποστηρίξτε Τώρα",
|
||||
"techStackTitle": "Εξέλιξη Τεχνολογίας",
|
||||
"coreContributorsTitle": "Κύριοι Συνεισφέροντες",
|
||||
"securityTitle": "Ασφάλεια",
|
||||
"designCreditsTitle": "Σχεδιασμός",
|
||||
"ideationTitle": "Ιδέες",
|
||||
"communityContributorsTitle": "Συνεισφέροντες Κοινότητας",
|
||||
@@ -2268,5 +2232,12 @@
|
||||
"submit": "Υποβολή",
|
||||
"communityExamples": "Παραδείγματα Κοινότητας",
|
||||
"userExample": "Παράδειγμα Χρήστη"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,45 +1025,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "or explore by industry",
|
||||
"clickToExplore": "Click to explore",
|
||||
"searchPlaceholder": "Search prompts...",
|
||||
"teachers": "Teachers",
|
||||
"developers": "Developers",
|
||||
"marketers": "Marketers",
|
||||
"designers": "Designers",
|
||||
"writers": "Writers",
|
||||
"analysts": "Analysts",
|
||||
"entrepreneurs": "Entrepreneurs",
|
||||
"researchers": "Researchers",
|
||||
"students": "Students",
|
||||
"consultants": "Consultants",
|
||||
"engineers": "Engineers",
|
||||
"creators": "Creators",
|
||||
"lawyers": "Lawyers",
|
||||
"doctors": "Doctors",
|
||||
"nurses": "Nurses",
|
||||
"accountants": "Accountants",
|
||||
"salespeople": "Salespeople",
|
||||
"recruiters": "Recruiters",
|
||||
"managers": "Managers",
|
||||
"executives": "Executives",
|
||||
"freelancers": "Freelancers",
|
||||
"photographers": "Photographers",
|
||||
"musicians": "Musicians",
|
||||
"artists": "Artists",
|
||||
"architects": "Architects",
|
||||
"scientists": "Scientists",
|
||||
"journalists": "Journalists",
|
||||
"editors": "Editors",
|
||||
"translators": "Translators",
|
||||
"coaches": "Coaches",
|
||||
"therapists": "Therapists",
|
||||
"trainers": "Trainers",
|
||||
"chefs": "Chefs",
|
||||
"realtors": "Realtors",
|
||||
"investors": "Investors",
|
||||
"traders": "Traders"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"promptBuilder": {
|
||||
"title": "Prompt Building Agent",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Support Now",
|
||||
"techStackTitle": "Tech Evolution",
|
||||
"coreContributorsTitle": "Core Contributors",
|
||||
"securityTitle": "Security",
|
||||
"designCreditsTitle": "Design",
|
||||
"ideationTitle": "Ideation",
|
||||
"communityContributorsTitle": "Community Contributors",
|
||||
@@ -2281,5 +2245,12 @@
|
||||
"submit": "Submit",
|
||||
"communityExamples": "Community Examples",
|
||||
"userExample": "User example"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "o explora por industria",
|
||||
"clickToExplore": "Haz clic para explorar",
|
||||
"searchPlaceholder": "Buscar prompts...",
|
||||
"teachers": "Profesores",
|
||||
"developers": "Desarrolladores",
|
||||
"marketers": "Marketers",
|
||||
"designers": "Diseñadores",
|
||||
"writers": "Escritores",
|
||||
"analysts": "Analistas",
|
||||
"entrepreneurs": "Emprendedores",
|
||||
"researchers": "Investigadores",
|
||||
"students": "Estudiantes",
|
||||
"consultants": "Consultores",
|
||||
"engineers": "Ingenieros",
|
||||
"creators": "Creadores",
|
||||
"lawyers": "Abogados",
|
||||
"doctors": "Médicos",
|
||||
"nurses": "Enfermeros",
|
||||
"accountants": "Contadores",
|
||||
"salespeople": "Vendedores",
|
||||
"recruiters": "Reclutadores",
|
||||
"managers": "Gerentes",
|
||||
"executives": "Ejecutivos",
|
||||
"freelancers": "Freelancers",
|
||||
"photographers": "Fotógrafos",
|
||||
"musicians": "Músicos",
|
||||
"artists": "Artistas",
|
||||
"architects": "Arquitectos",
|
||||
"scientists": "Científicos",
|
||||
"journalists": "Periodistas",
|
||||
"editors": "Editores",
|
||||
"translators": "Traductores",
|
||||
"coaches": "Coaches",
|
||||
"therapists": "Terapeutas",
|
||||
"trainers": "Entrenadores",
|
||||
"chefs": "Chefs",
|
||||
"realtors": "Agentes",
|
||||
"investors": "Inversores",
|
||||
"traders": "Traders"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Página No Encontrada",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Apoyar Ahora",
|
||||
"techStackTitle": "Evolución Tecnológica",
|
||||
"coreContributorsTitle": "Contribuidores Principales",
|
||||
"securityTitle": "Seguridad",
|
||||
"designCreditsTitle": "Diseño",
|
||||
"ideationTitle": "Ideación",
|
||||
"communityContributorsTitle": "Contribuidores de la Comunidad",
|
||||
@@ -2280,5 +2244,12 @@
|
||||
"submit": "Enviar",
|
||||
"communityExamples": "Ejemplos de la Comunidad",
|
||||
"userExample": "Ejemplo de usuario"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,45 +1025,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "یا بر اساس صنعت کاوش کنید",
|
||||
"clickToExplore": "برای کاوش کلیک کنید",
|
||||
"searchPlaceholder": "جستجوی پرامپت...",
|
||||
"teachers": "معلمان",
|
||||
"developers": "توسعهدهندگان",
|
||||
"marketers": "بازاریابان",
|
||||
"designers": "طراحان",
|
||||
"writers": "نویسندگان",
|
||||
"analysts": "تحلیلگران",
|
||||
"entrepreneurs": "کارآفرینان",
|
||||
"researchers": "پژوهشگران",
|
||||
"students": "دانشجویان",
|
||||
"consultants": "مشاوران",
|
||||
"engineers": "مهندسان",
|
||||
"creators": "خالقان",
|
||||
"lawyers": "وکلا",
|
||||
"doctors": "پزشکان",
|
||||
"nurses": "پرستاران",
|
||||
"accountants": "حسابداران",
|
||||
"salespeople": "فروشندگان",
|
||||
"recruiters": "استخدامکنندگان",
|
||||
"managers": "مدیران",
|
||||
"executives": "مدیران ارشد",
|
||||
"freelancers": "فریلنسرها",
|
||||
"photographers": "عکاسان",
|
||||
"musicians": "موسیقیدانان",
|
||||
"artists": "هنرمندان",
|
||||
"architects": "معماران",
|
||||
"scientists": "دانشمندان",
|
||||
"journalists": "روزنامهنگاران",
|
||||
"editors": "ویراستاران",
|
||||
"translators": "مترجمان",
|
||||
"coaches": "مربیان",
|
||||
"therapists": "درمانگران",
|
||||
"trainers": "آموزشدهندگان",
|
||||
"chefs": "سرآشپزها",
|
||||
"realtors": "مشاوران املاک",
|
||||
"investors": "سرمایهگذاران",
|
||||
"traders": "معاملهگران"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"promptBuilder": {
|
||||
"title": "عامل ساخت پرامپت",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "همین الان حمایت کنید",
|
||||
"techStackTitle": "تکامل فناوری",
|
||||
"coreContributorsTitle": "مشارکتکنندگان اصلی",
|
||||
"securityTitle": "امنیت",
|
||||
"designCreditsTitle": "طراحی",
|
||||
"ideationTitle": "ایدهپردازی",
|
||||
"communityContributorsTitle": "مشارکتکنندگان جامعه",
|
||||
@@ -2261,5 +2225,12 @@
|
||||
"submit": "ارسال",
|
||||
"communityExamples": "مثالهای جامعه",
|
||||
"userExample": "مثال کاربر"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "ou explorez par secteur",
|
||||
"clickToExplore": "Cliquez pour explorer",
|
||||
"searchPlaceholder": "Rechercher des prompts...",
|
||||
"teachers": "Enseignants",
|
||||
"developers": "Développeurs",
|
||||
"marketers": "Marketeurs",
|
||||
"designers": "Designers",
|
||||
"writers": "Écrivains",
|
||||
"analysts": "Analystes",
|
||||
"entrepreneurs": "Entrepreneurs",
|
||||
"researchers": "Chercheurs",
|
||||
"students": "Étudiants",
|
||||
"consultants": "Consultants",
|
||||
"engineers": "Ingénieurs",
|
||||
"creators": "Créateurs",
|
||||
"lawyers": "Avocats",
|
||||
"doctors": "Médecins",
|
||||
"nurses": "Infirmiers",
|
||||
"accountants": "Comptables",
|
||||
"salespeople": "Commerciaux",
|
||||
"recruiters": "Recruteurs",
|
||||
"managers": "Managers",
|
||||
"executives": "Dirigeants",
|
||||
"freelancers": "Freelances",
|
||||
"photographers": "Photographes",
|
||||
"musicians": "Musiciens",
|
||||
"artists": "Artistes",
|
||||
"architects": "Architectes",
|
||||
"scientists": "Scientifiques",
|
||||
"journalists": "Journalistes",
|
||||
"editors": "Rédacteurs",
|
||||
"translators": "Traducteurs",
|
||||
"coaches": "Coachs",
|
||||
"therapists": "Thérapeutes",
|
||||
"trainers": "Formateurs",
|
||||
"chefs": "Chefs",
|
||||
"realtors": "Agents",
|
||||
"investors": "Investisseurs",
|
||||
"traders": "Traders"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page Non Trouvée",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Soutenir Maintenant",
|
||||
"techStackTitle": "Évolution Technologique",
|
||||
"coreContributorsTitle": "Contributeurs Principaux",
|
||||
"securityTitle": "Sécurité",
|
||||
"designCreditsTitle": "Design",
|
||||
"ideationTitle": "Idéation",
|
||||
"communityContributorsTitle": "Contributeurs de la Communauté",
|
||||
@@ -2280,5 +2244,12 @@
|
||||
"submit": "Soumettre",
|
||||
"communityExamples": "Exemples de la Communauté",
|
||||
"userExample": "Exemple utilisateur"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1051,45 +1051,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "או חקור לפי תעשייה",
|
||||
"clickToExplore": "לחץ לחקירה",
|
||||
"searchPlaceholder": "חפש פרומפטים...",
|
||||
"teachers": "מורים",
|
||||
"developers": "מפתחים",
|
||||
"marketers": "משווקים",
|
||||
"designers": "מעצבים",
|
||||
"writers": "סופרים",
|
||||
"analysts": "אנליסטים",
|
||||
"entrepreneurs": "יזמים",
|
||||
"researchers": "חוקרים",
|
||||
"students": "סטודנטים",
|
||||
"consultants": "יועצים",
|
||||
"engineers": "מהנדסים",
|
||||
"creators": "יוצרים",
|
||||
"lawyers": "עורכי דין",
|
||||
"doctors": "רופאים",
|
||||
"nurses": "אחיות",
|
||||
"accountants": "רואי חשבון",
|
||||
"salespeople": "אנשי מכירות",
|
||||
"recruiters": "מגייסים",
|
||||
"managers": "מנהלים",
|
||||
"executives": "בכירים",
|
||||
"freelancers": "פרילנסרים",
|
||||
"photographers": "צלמים",
|
||||
"musicians": "מוזיקאים",
|
||||
"artists": "אמנים",
|
||||
"architects": "אדריכלים",
|
||||
"scientists": "מדענים",
|
||||
"journalists": "עיתונאים",
|
||||
"editors": "עורכים",
|
||||
"translators": "מתרגמים",
|
||||
"coaches": "מאמנים",
|
||||
"therapists": "מטפלים",
|
||||
"trainers": "מדריכים",
|
||||
"chefs": "שפים",
|
||||
"realtors": "סוכני נדל״ן",
|
||||
"investors": "משקיעים",
|
||||
"traders": "סוחרים"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"promptBuilder": {
|
||||
"title": "סוכן בניית פרומפטים",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "תמכו עכשיו",
|
||||
"techStackTitle": "התפתחות טכנולוגית",
|
||||
"coreContributorsTitle": "תורמים מרכזיים",
|
||||
"securityTitle": "אבטחה",
|
||||
"designCreditsTitle": "עיצוב",
|
||||
"ideationTitle": "רעיונות",
|
||||
"communityContributorsTitle": "תורמי קהילה",
|
||||
@@ -2261,5 +2225,12 @@
|
||||
"submit": "שלח",
|
||||
"communityExamples": "דוגמאות קהילה",
|
||||
"userExample": "דוגמת משתמש"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "o esplora per settore",
|
||||
"clickToExplore": "Clicca per esplorare",
|
||||
"searchPlaceholder": "Cerca prompt...",
|
||||
"teachers": "Insegnanti",
|
||||
"developers": "Sviluppatori",
|
||||
"marketers": "Marketer",
|
||||
"designers": "Designer",
|
||||
"writers": "Scrittori",
|
||||
"analysts": "Analisti",
|
||||
"entrepreneurs": "Imprenditori",
|
||||
"researchers": "Ricercatori",
|
||||
"students": "Studenti",
|
||||
"consultants": "Consulenti",
|
||||
"engineers": "Ingegneri",
|
||||
"creators": "Creatori",
|
||||
"lawyers": "Avvocati",
|
||||
"doctors": "Medici",
|
||||
"nurses": "Infermieri",
|
||||
"accountants": "Contabili",
|
||||
"salespeople": "Venditori",
|
||||
"recruiters": "Recruiter",
|
||||
"managers": "Manager",
|
||||
"executives": "Dirigenti",
|
||||
"freelancers": "Freelancer",
|
||||
"photographers": "Fotografi",
|
||||
"musicians": "Musicisti",
|
||||
"artists": "Artisti",
|
||||
"architects": "Architetti",
|
||||
"scientists": "Scienziati",
|
||||
"journalists": "Giornalisti",
|
||||
"editors": "Redattori",
|
||||
"translators": "Traduttori",
|
||||
"coaches": "Coach",
|
||||
"therapists": "Terapeuti",
|
||||
"trainers": "Formatori",
|
||||
"chefs": "Chef",
|
||||
"realtors": "Agenti",
|
||||
"investors": "Investitori",
|
||||
"traders": "Trader"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Pagina Non Trovata",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Supporta Ora",
|
||||
"techStackTitle": "Evoluzione Tecnologica",
|
||||
"coreContributorsTitle": "Contributori Principali",
|
||||
"securityTitle": "Sicurezza",
|
||||
"designCreditsTitle": "Design",
|
||||
"ideationTitle": "Ideazione",
|
||||
"communityContributorsTitle": "Contributori della Community",
|
||||
@@ -2283,5 +2247,12 @@
|
||||
"submit": "Invia",
|
||||
"communityExamples": "Esempi della Community",
|
||||
"userExample": "Esempio utente"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "または業種で探す",
|
||||
"clickToExplore": "クリックして探索",
|
||||
"searchPlaceholder": "プロンプトを検索...",
|
||||
"teachers": "教師",
|
||||
"developers": "開発者",
|
||||
"marketers": "マーケター",
|
||||
"designers": "デザイナー",
|
||||
"writers": "ライター",
|
||||
"analysts": "アナリスト",
|
||||
"entrepreneurs": "起業家",
|
||||
"researchers": "研究者",
|
||||
"students": "学生",
|
||||
"consultants": "コンサルタント",
|
||||
"engineers": "エンジニア",
|
||||
"creators": "クリエイター",
|
||||
"lawyers": "弁護士",
|
||||
"doctors": "医師",
|
||||
"nurses": "看護師",
|
||||
"accountants": "会計士",
|
||||
"salespeople": "営業",
|
||||
"recruiters": "採用担当",
|
||||
"managers": "マネージャー",
|
||||
"executives": "経営者",
|
||||
"freelancers": "フリーランス",
|
||||
"photographers": "写真家",
|
||||
"musicians": "ミュージシャン",
|
||||
"artists": "アーティスト",
|
||||
"architects": "建築家",
|
||||
"scientists": "科学者",
|
||||
"journalists": "ジャーナリスト",
|
||||
"editors": "編集者",
|
||||
"translators": "翻訳者",
|
||||
"coaches": "コーチ",
|
||||
"therapists": "セラピスト",
|
||||
"trainers": "トレーナー",
|
||||
"chefs": "シェフ",
|
||||
"realtors": "不動産業者",
|
||||
"investors": "投資家",
|
||||
"traders": "トレーダー"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "ページが見つかりません",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "今すぐサポート",
|
||||
"techStackTitle": "技術スタックの進化",
|
||||
"coreContributorsTitle": "コアコントリビューター",
|
||||
"securityTitle": "セキュリティ",
|
||||
"designCreditsTitle": "デザイン",
|
||||
"ideationTitle": "アイデア",
|
||||
"communityContributorsTitle": "コミュニティコントリビューター",
|
||||
@@ -2265,5 +2229,12 @@
|
||||
"submit": "送信",
|
||||
"communityExamples": "コミュニティの例",
|
||||
"userExample": "ユーザーの例"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "또는 업종별 탐색",
|
||||
"clickToExplore": "클릭하여 탐색",
|
||||
"searchPlaceholder": "프롬프트 검색...",
|
||||
"teachers": "교사",
|
||||
"developers": "개발자",
|
||||
"marketers": "마케터",
|
||||
"designers": "디자이너",
|
||||
"writers": "작가",
|
||||
"analysts": "분석가",
|
||||
"entrepreneurs": "창업가",
|
||||
"researchers": "연구자",
|
||||
"students": "학생",
|
||||
"consultants": "컨설턴트",
|
||||
"engineers": "엔지니어",
|
||||
"creators": "크리에이터",
|
||||
"lawyers": "변호사",
|
||||
"doctors": "의사",
|
||||
"nurses": "간호사",
|
||||
"accountants": "회계사",
|
||||
"salespeople": "영업사원",
|
||||
"recruiters": "채용담당자",
|
||||
"managers": "매니저",
|
||||
"executives": "임원",
|
||||
"freelancers": "프리랜서",
|
||||
"photographers": "사진작가",
|
||||
"musicians": "음악가",
|
||||
"artists": "아티스트",
|
||||
"architects": "건축가",
|
||||
"scientists": "과학자",
|
||||
"journalists": "기자",
|
||||
"editors": "편집자",
|
||||
"translators": "번역가",
|
||||
"coaches": "코치",
|
||||
"therapists": "치료사",
|
||||
"trainers": "트레이너",
|
||||
"chefs": "셰프",
|
||||
"realtors": "부동산",
|
||||
"investors": "투자자",
|
||||
"traders": "트레이더"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "페이지를 찾을 수 없습니다",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "지금 후원하기",
|
||||
"techStackTitle": "기술 스택 진화",
|
||||
"coreContributorsTitle": "핵심 기여자",
|
||||
"securityTitle": "보안",
|
||||
"designCreditsTitle": "디자인",
|
||||
"ideationTitle": "아이디어",
|
||||
"communityContributorsTitle": "커뮤니티 기여자",
|
||||
@@ -2265,5 +2229,12 @@
|
||||
"submit": "제출",
|
||||
"communityExamples": "커뮤니티 예제",
|
||||
"userExample": "사용자 예제"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,45 +1025,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "of verken per industrie",
|
||||
"clickToExplore": "Klik om te verkennen",
|
||||
"searchPlaceholder": "Prompts zoeken...",
|
||||
"teachers": "Docenten",
|
||||
"developers": "Ontwikkelaars",
|
||||
"marketers": "Marketeers",
|
||||
"designers": "Ontwerpers",
|
||||
"writers": "Schrijvers",
|
||||
"analysts": "Analisten",
|
||||
"entrepreneurs": "Ondernemers",
|
||||
"researchers": "Onderzoekers",
|
||||
"students": "Studenten",
|
||||
"consultants": "Consultants",
|
||||
"engineers": "Ingenieurs",
|
||||
"creators": "Creators",
|
||||
"lawyers": "Advocaten",
|
||||
"doctors": "Artsen",
|
||||
"nurses": "Verpleegkundigen",
|
||||
"accountants": "Accountants",
|
||||
"salespeople": "Verkopers",
|
||||
"recruiters": "Recruiters",
|
||||
"managers": "Managers",
|
||||
"executives": "Directeuren",
|
||||
"freelancers": "Freelancers",
|
||||
"photographers": "Fotografen",
|
||||
"musicians": "Muzikanten",
|
||||
"artists": "Kunstenaars",
|
||||
"architects": "Architecten",
|
||||
"scientists": "Wetenschappers",
|
||||
"journalists": "Journalisten",
|
||||
"editors": "Redacteuren",
|
||||
"translators": "Vertalers",
|
||||
"coaches": "Coaches",
|
||||
"therapists": "Therapeuten",
|
||||
"trainers": "Trainers",
|
||||
"chefs": "Chefs",
|
||||
"realtors": "Makelaars",
|
||||
"investors": "Investeerders",
|
||||
"traders": "Handelaren"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"promptBuilder": {
|
||||
"title": "Prompt Building Agent",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Steun nu",
|
||||
"techStackTitle": "Tech-evolutie",
|
||||
"coreContributorsTitle": "Kernbijdragers",
|
||||
"securityTitle": "Beveiliging",
|
||||
"designCreditsTitle": "Ontwerp",
|
||||
"ideationTitle": "Ideeën",
|
||||
"communityContributorsTitle": "Community-bijdragers",
|
||||
@@ -2268,5 +2232,12 @@
|
||||
"submit": "Versturen",
|
||||
"communityExamples": "Community Voorbeelden",
|
||||
"userExample": "Gebruikers Voorbeeld"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "ou explore por setor",
|
||||
"clickToExplore": "Clique para explorar",
|
||||
"searchPlaceholder": "Pesquisar prompts...",
|
||||
"teachers": "Professores",
|
||||
"developers": "Desenvolvedores",
|
||||
"marketers": "Marketers",
|
||||
"designers": "Designers",
|
||||
"writers": "Escritores",
|
||||
"analysts": "Analistas",
|
||||
"entrepreneurs": "Empreendedores",
|
||||
"researchers": "Pesquisadores",
|
||||
"students": "Estudantes",
|
||||
"consultants": "Consultores",
|
||||
"engineers": "Engenheiros",
|
||||
"creators": "Criadores",
|
||||
"lawyers": "Advogados",
|
||||
"doctors": "Médicos",
|
||||
"nurses": "Enfermeiros",
|
||||
"accountants": "Contadores",
|
||||
"salespeople": "Vendedores",
|
||||
"recruiters": "Recrutadores",
|
||||
"managers": "Gerentes",
|
||||
"executives": "Executivos",
|
||||
"freelancers": "Freelancers",
|
||||
"photographers": "Fotógrafos",
|
||||
"musicians": "Músicos",
|
||||
"artists": "Artistas",
|
||||
"architects": "Arquitetos",
|
||||
"scientists": "Cientistas",
|
||||
"journalists": "Jornalistas",
|
||||
"editors": "Editores",
|
||||
"translators": "Tradutores",
|
||||
"coaches": "Coaches",
|
||||
"therapists": "Terapeutas",
|
||||
"trainers": "Treinadores",
|
||||
"chefs": "Chefs",
|
||||
"realtors": "Corretores",
|
||||
"investors": "Investidores",
|
||||
"traders": "Traders"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Página Não Encontrada",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Apoiar Agora",
|
||||
"techStackTitle": "Evolução Tecnológica",
|
||||
"coreContributorsTitle": "Contribuidores Principais",
|
||||
"securityTitle": "Segurança",
|
||||
"designCreditsTitle": "Design",
|
||||
"ideationTitle": "Ideação",
|
||||
"communityContributorsTitle": "Contribuidores da Comunidade",
|
||||
@@ -2279,5 +2243,12 @@
|
||||
"submit": "Enviar",
|
||||
"communityExamples": "Exemplos da Comunidade",
|
||||
"userExample": "Exemplo do usuário"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1051,45 +1051,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "или исследуйте по отрасли",
|
||||
"clickToExplore": "Нажмите для изучения",
|
||||
"searchPlaceholder": "Поиск промптов...",
|
||||
"teachers": "Учителей",
|
||||
"developers": "Разработчиков",
|
||||
"marketers": "Маркетологов",
|
||||
"designers": "Дизайнеров",
|
||||
"writers": "Писателей",
|
||||
"analysts": "Аналитиков",
|
||||
"entrepreneurs": "Предпринимателей",
|
||||
"researchers": "Исследователей",
|
||||
"students": "Студентов",
|
||||
"consultants": "Консультантов",
|
||||
"engineers": "Инженеров",
|
||||
"creators": "Создателей",
|
||||
"lawyers": "Юристов",
|
||||
"doctors": "Врачей",
|
||||
"nurses": "Медсестёр",
|
||||
"accountants": "Бухгалтеров",
|
||||
"salespeople": "Продавцов",
|
||||
"recruiters": "Рекрутеров",
|
||||
"managers": "Менеджеров",
|
||||
"executives": "Руководителей",
|
||||
"freelancers": "Фрилансеров",
|
||||
"photographers": "Фотографов",
|
||||
"musicians": "Музыкантов",
|
||||
"artists": "Художников",
|
||||
"architects": "Архитекторов",
|
||||
"scientists": "Учёных",
|
||||
"journalists": "Журналистов",
|
||||
"editors": "Редакторов",
|
||||
"translators": "Переводчиков",
|
||||
"coaches": "Коучей",
|
||||
"therapists": "Терапевтов",
|
||||
"trainers": "Тренеров",
|
||||
"chefs": "Поваров",
|
||||
"realtors": "Риелторов",
|
||||
"investors": "Инвесторов",
|
||||
"traders": "Трейдеров"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"promptBuilder": {
|
||||
"title": "Агент создания промптов",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Поддержать сейчас",
|
||||
"techStackTitle": "Эволюция технологий",
|
||||
"coreContributorsTitle": "Основные участники",
|
||||
"securityTitle": "Безопасность",
|
||||
"designCreditsTitle": "Дизайн",
|
||||
"ideationTitle": "Идеи",
|
||||
"communityContributorsTitle": "Участники сообщества",
|
||||
@@ -2262,5 +2226,12 @@
|
||||
"submit": "Отправить",
|
||||
"communityExamples": "Примеры сообщества",
|
||||
"userExample": "Пример пользователя"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "veya sektöre göre keşfet",
|
||||
"clickToExplore": "Keşfetmek için tıkla",
|
||||
"searchPlaceholder": "Prompt ara...",
|
||||
"teachers": "Öğretmenler",
|
||||
"developers": "Geliştiriciler",
|
||||
"marketers": "Pazarlamacılar",
|
||||
"designers": "Tasarımcılar",
|
||||
"writers": "Yazarlar",
|
||||
"analysts": "Analistler",
|
||||
"entrepreneurs": "Girişimciler",
|
||||
"researchers": "Araştırmacılar",
|
||||
"students": "Öğrenciler",
|
||||
"consultants": "Danışmanlar",
|
||||
"engineers": "Mühendisler",
|
||||
"creators": "İçerik Üreticileri",
|
||||
"lawyers": "Avukatlar",
|
||||
"doctors": "Doktorlar",
|
||||
"nurses": "Hemşireler",
|
||||
"accountants": "Muhasebeciler",
|
||||
"salespeople": "Satışçılar",
|
||||
"recruiters": "İK Uzmanları",
|
||||
"managers": "Yöneticiler",
|
||||
"executives": "Üst Yöneticiler",
|
||||
"freelancers": "Serbest Çalışanlar",
|
||||
"photographers": "Fotoğrafçılar",
|
||||
"musicians": "Müzisyenler",
|
||||
"artists": "Sanatçılar",
|
||||
"architects": "Mimarlar",
|
||||
"scientists": "Bilim İnsanları",
|
||||
"journalists": "Gazeteciler",
|
||||
"editors": "Editörler",
|
||||
"translators": "Çevirmenler",
|
||||
"coaches": "Koçlar",
|
||||
"therapists": "Terapistler",
|
||||
"trainers": "Eğitmenler",
|
||||
"chefs": "Şefler",
|
||||
"realtors": "Emlakçılar",
|
||||
"investors": "Yatırımcılar",
|
||||
"traders": "Tüccarlar"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Sayfa Bulunamadı",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "Şimdi Destekle",
|
||||
"techStackTitle": "Teknoloji Evrimi",
|
||||
"coreContributorsTitle": "Çekirdek Katkıda Bulunanlar",
|
||||
"securityTitle": "Güvenlik",
|
||||
"designCreditsTitle": "Tasarım",
|
||||
"ideationTitle": "Fikir",
|
||||
"communityContributorsTitle": "Topluluk Katkıda Bulunanları",
|
||||
@@ -2276,5 +2240,12 @@
|
||||
"submit": "Gönder",
|
||||
"communityExamples": "Topluluk Örnekleri",
|
||||
"userExample": "Kullanıcı örneği"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,45 +1030,8 @@
|
||||
}
|
||||
},
|
||||
"heroIndustries": {
|
||||
"prefix": "或按行业探索",
|
||||
"clickToExplore": "点击探索",
|
||||
"searchPlaceholder": "搜索提示词...",
|
||||
"teachers": "教师",
|
||||
"developers": "开发者",
|
||||
"marketers": "营销人员",
|
||||
"designers": "设计师",
|
||||
"writers": "作家",
|
||||
"analysts": "分析师",
|
||||
"entrepreneurs": "创业者",
|
||||
"researchers": "研究员",
|
||||
"students": "学生",
|
||||
"consultants": "顾问",
|
||||
"engineers": "工程师",
|
||||
"creators": "创作者",
|
||||
"lawyers": "律师",
|
||||
"doctors": "医生",
|
||||
"nurses": "护士",
|
||||
"accountants": "会计",
|
||||
"salespeople": "销售",
|
||||
"recruiters": "招聘官",
|
||||
"managers": "经理",
|
||||
"executives": "高管",
|
||||
"freelancers": "自由职业者",
|
||||
"photographers": "摄影师",
|
||||
"musicians": "音乐家",
|
||||
"artists": "艺术家",
|
||||
"architects": "建筑师",
|
||||
"scientists": "科学家",
|
||||
"journalists": "记者",
|
||||
"editors": "编辑",
|
||||
"translators": "翻译",
|
||||
"coaches": "教练",
|
||||
"therapists": "治疗师",
|
||||
"trainers": "培训师",
|
||||
"chefs": "厨师",
|
||||
"realtors": "房产经纪",
|
||||
"investors": "投资者",
|
||||
"traders": "交易员"
|
||||
"videoTitle": "GitHub Open Source Friday"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "页面未找到",
|
||||
@@ -1489,6 +1452,7 @@
|
||||
"supportNow": "立即支持",
|
||||
"techStackTitle": "技术演进",
|
||||
"coreContributorsTitle": "核心贡献者",
|
||||
"securityTitle": "安全",
|
||||
"designCreditsTitle": "设计",
|
||||
"ideationTitle": "创意",
|
||||
"communityContributorsTitle": "社区贡献者",
|
||||
@@ -2265,5 +2229,12 @@
|
||||
"submit": "提交",
|
||||
"communityExamples": "社区示例",
|
||||
"userExample": "用户示例"
|
||||
},
|
||||
"selfHosting": {
|
||||
"prerequisites": {
|
||||
"nodeJs": "Node.js 24.x",
|
||||
"postgresql": "PostgreSQL database",
|
||||
"npm": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +82,10 @@ export default withSentryConfig(withMDX(withNextIntl(nextConfig)), {
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: "/monitoring",
|
||||
// tunnelRoute removed — was proxying all browser Sentry events through Vercel edge,
|
||||
// generating unnecessary edge requests ($2.45/M) and function invocations ($0.60/M).
|
||||
// Estimated savings: $100-400/month. Trade-off: some ad-blockers may block direct Sentry calls.
|
||||
// See: https://github.com/f/prompts.chat/issues/1085
|
||||
|
||||
webpack: {
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
|
||||
@@ -2375,7 +2375,7 @@ import type {
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js** 18+
|
||||
- **Node.js** 24.x
|
||||
- **TypeScript** 5+ (optional, for type checking)
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
const PROMPTS_URL = 'https://prompts.chat/prompts.json';
|
||||
const PROMPTS_URL = 'https://prompts.chat/prompts.json?full_content=true';
|
||||
const CACHE_DIR = join(homedir(), '.prompts-chat');
|
||||
const CACHE_FILE = join(CACHE_DIR, 'prompts.json');
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export function PromptDetail({ prompt, onBack, onCopy }: PromptDetailProps) {
|
||||
const contentHeight = Math.max(terminalHeight - headerLines - footerLines, 5);
|
||||
|
||||
useEffect(() => {
|
||||
const vars = extractVariables(prompt.content);
|
||||
const vars = extractVariables(prompt.content ?? '');
|
||||
setVariables(vars);
|
||||
|
||||
const defaults: Record<string, string> = {};
|
||||
@@ -90,7 +90,7 @@ export function PromptDetail({ prompt, onBack, onCopy }: PromptDetailProps) {
|
||||
const contentLines = useMemo(() => {
|
||||
if (!prompt) return [];
|
||||
// Parse escape sequences like \n
|
||||
const parsedContent = prompt.content.replace(/\\n/g, '\n');
|
||||
const parsedContent = (prompt.content ?? '').replace(/\\n/g, '\n');
|
||||
return wrapText(parsedContent, terminalWidth - 6);
|
||||
}, [prompt, terminalWidth]);
|
||||
|
||||
@@ -200,12 +200,12 @@ export function PromptDetail({ prompt, onBack, onCopy }: PromptDetailProps) {
|
||||
setCurrentVarIndex(0);
|
||||
setCurrentInput(variableValues[variables[0].name] || '');
|
||||
} else {
|
||||
handleCopy(prompt.content);
|
||||
handleCopy(prompt.content ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if (input === 'C' && prompt) {
|
||||
handleCopy(prompt.content);
|
||||
handleCopy(prompt.content ?? '');
|
||||
}
|
||||
|
||||
if (input === 'o' && prompt) {
|
||||
|
||||
@@ -31,6 +31,7 @@ export const codePlatforms: Platform[] = [
|
||||
{ id: "goose", name: "Goose", baseUrl: "goose://recipe", isDeeplink: true },
|
||||
{ id: "github-copilot", name: "GitHub Copilot Chat", baseUrl: "https://github.com/copilot" },
|
||||
{ id: "github-copilot-agents", name: "GitHub Copilot Agents", baseUrl: "https://github.com/copilot/agents" },
|
||||
{ id: "netlify", name: "Netlify", baseUrl: "https://app.netlify.com/run" },
|
||||
{ id: "bolt", name: "Bolt", baseUrl: "https://bolt.new" },
|
||||
{ id: "lovable", name: "Lovable", baseUrl: "https://lovable.dev" },
|
||||
{ id: "v0", name: "v0", baseUrl: "https://v0.dev/chat" },
|
||||
@@ -103,6 +104,8 @@ export function buildUrl(
|
||||
return `${baseUrl}/?prompt=${encoded}`;
|
||||
case "lovable":
|
||||
return `${baseUrl}/?autosubmit=true#prompt=${encoded}`;
|
||||
case "netlify":
|
||||
return `${baseUrl}?prompt=${encoded}&ref=prompts-chat`;
|
||||
case "mistral":
|
||||
return `${baseUrl}?q=${encoded}`;
|
||||
case "perplexity":
|
||||
|
||||
@@ -192,6 +192,12 @@ export const codePlatforms: Platform[] = [
|
||||
baseUrl: "https://github.com/copilot",
|
||||
supportsQuerystring: true,
|
||||
},
|
||||
{
|
||||
id: "netlify",
|
||||
name: "Netlify",
|
||||
baseUrl: "https://app.netlify.com/run",
|
||||
supportsQuerystring: true,
|
||||
},
|
||||
{
|
||||
id: "bolt",
|
||||
name: "Bolt",
|
||||
@@ -278,6 +284,8 @@ export function buildUrl(
|
||||
return `${baseUrl}/?prompt=${encoded}`;
|
||||
case "lovable":
|
||||
return `${baseUrl}/?autosubmit=true#prompt=${encoded}`;
|
||||
case "netlify":
|
||||
return `${baseUrl}?prompt=${encoded}&ref=prompts-chat`;
|
||||
case "mistral":
|
||||
return `${baseUrl}?q=${encoded}`;
|
||||
case "perplexity":
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add case-insensitive unique indexes to prevent TOCTOU race conditions
|
||||
-- These ensure the database enforces uniqueness regardless of casing
|
||||
CREATE UNIQUE INDEX "users_username_ci_unique" ON "users" (LOWER("username"));
|
||||
CREATE UNIQUE INDEX "users_email_ci_unique" ON "users" (LOWER("email"));
|
||||
@@ -139,6 +139,8 @@ model Prompt {
|
||||
@@index([isPrivate])
|
||||
@@index([isFeatured])
|
||||
@@index([isUnlisted])
|
||||
@@index([slug])
|
||||
@@index([isPrivate, isUnlisted, deletedAt, createdAt(sort: Desc)])
|
||||
@@map("prompts")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@ import bcrypt from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const PROMPTS_JSON_URL = "https://prompts.chat/prompts.json";
|
||||
const PROMPTS_JSON_URL = "https://prompts.chat/prompts.json?full_content=true";
|
||||
|
||||
interface RemotePrompt {
|
||||
/** Raw shape returned by the remote API — content may be absent. */
|
||||
interface RemotePromptRaw {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
content: string;
|
||||
content?: string;
|
||||
type: string;
|
||||
structuredFormat: string | null;
|
||||
mediaUrl: string | null;
|
||||
@@ -52,11 +53,20 @@ interface RemotePrompt {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RemotePromptsResponse {
|
||||
count: number;
|
||||
prompts: RemotePrompt[];
|
||||
/** Validated prompt with required content field. */
|
||||
type RemotePrompt = RemotePromptRaw & { content: string };
|
||||
|
||||
/** Type guard that narrows a raw API prompt to a validated RemotePrompt. */
|
||||
function hasContent(prompt: RemotePromptRaw): prompt is RemotePrompt {
|
||||
return typeof prompt.content === "string" && prompt.content.length > 0;
|
||||
}
|
||||
|
||||
interface RemotePromptsResponse {
|
||||
count: number;
|
||||
prompts: RemotePromptRaw[];
|
||||
}
|
||||
|
||||
/** Fetches all prompts from the remote prompts.chat API with full content. */
|
||||
async function fetchPrompts(): Promise<RemotePromptsResponse> {
|
||||
console.log(`📡 Fetching prompts from ${PROMPTS_JSON_URL}...`);
|
||||
const response = await fetch(PROMPTS_JSON_URL);
|
||||
@@ -68,12 +78,13 @@ async function fetchPrompts(): Promise<RemotePromptsResponse> {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Seeds the database with users, categories, tags, and prompts from prompts.chat. */
|
||||
async function main() {
|
||||
console.log("🌱 Seeding database from prompts.chat...");
|
||||
|
||||
// Create admin user for assigning prompts
|
||||
const password = await bcrypt.hash("password123", 12);
|
||||
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: "admin@prompts.chat" },
|
||||
update: {},
|
||||
@@ -174,7 +185,7 @@ async function main() {
|
||||
userIdMap.set(username, admin.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { username },
|
||||
update: { name: author.name, avatar: author.avatar },
|
||||
@@ -225,6 +236,13 @@ async function main() {
|
||||
};
|
||||
const structuredFormat = remotePrompt.structuredFormat ? formatMap[remotePrompt.structuredFormat] : null;
|
||||
|
||||
// Skip prompts without content
|
||||
if (!hasContent(remotePrompt)) {
|
||||
console.warn(`⚠ Skipping prompt "${remotePrompt.title}" - no content`);
|
||||
promptsSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if prompt already exists
|
||||
const existingPrompt = await prisma.prompt.findFirst({
|
||||
where: { slug: remotePrompt.slug },
|
||||
@@ -280,6 +298,16 @@ async function main() {
|
||||
}
|
||||
|
||||
console.log(`✅ Created ${promptsCreated} prompts (${promptsSkipped} skipped)`);
|
||||
|
||||
// Fail-fast if no prompts were created — likely an upstream API regression
|
||||
const total = promptsCreated + promptsSkipped;
|
||||
if (total > 0 && promptsCreated === 0) {
|
||||
throw new Error(
|
||||
`Seeding failed: 0 prompts created out of ${total} (${promptsSkipped} skipped). ` +
|
||||
`This likely indicates an upstream API regression — check that ${PROMPTS_JSON_URL} returns content.`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("\n🎉 Seeding complete!");
|
||||
console.log("\n📋 Test credentials (password: password123):");
|
||||
console.log(" Admin: admin@prompts.chat");
|
||||
|
||||
@@ -33,7 +33,7 @@ export default defineConfig({
|
||||
|
||||
// Authentication plugins
|
||||
auth: {
|
||||
// Available: "credentials" | "google" | "azure" | "github" | "apple" | custom
|
||||
// Available: "credentials" | "google" | "azure" | "github" | "apple" | "oidc" | "oauth" | custom
|
||||
// Use `providers` array to enable multiple auth providers
|
||||
providers: ["github", "google", "apple"],
|
||||
// Allow public registration (only applies to credentials provider)
|
||||
@@ -57,11 +57,11 @@ export default defineConfig({
|
||||
// Enable tags
|
||||
tags: true,
|
||||
// Enable AI-powered semantic search (requires OPENAI_API_KEY)
|
||||
aiSearch: false,
|
||||
aiSearch: true,
|
||||
// Enable AI-powered generation features (requires OPENAI_API_KEY)
|
||||
aiGeneration: false,
|
||||
aiGeneration: true,
|
||||
// Enable MCP (Model Context Protocol) features including API key generation
|
||||
mcp: false,
|
||||
mcp: true,
|
||||
// Enable comments on prompts
|
||||
comments: true,
|
||||
},
|
||||
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 177 KiB |
@@ -0,0 +1,44 @@
|
||||
<svg width="240" height="24" viewBox="0 0 240 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.4283 0.610352L26.8565 23.8046H0L13.4283 0.610352Z" fill="#fff"/>
|
||||
<path d="M236.436 22.7547L235.081 17.1148V23.8045H233.849V15.1372H235.557L236.924 20.7405L238.291 15.1372H240V23.8045H238.767V17.1148L237.412 22.7547H236.436Z" fill="#fff"/>
|
||||
<path d="M226.237 23.8045L228.776 15.1372H230.436L232.974 23.8045H231.644L231.022 21.5462H228.19L227.568 23.8045H226.237ZM229.606 16.4678L228.52 20.362H230.692L229.606 16.4678Z" fill="#fff"/>
|
||||
<path d="M219.444 23.8045V15.1372H222.142C223.972 15.1372 225.095 16.126 225.095 17.7252C225.095 18.7628 224.436 19.5197 223.631 19.7761C224.485 19.9348 224.9 20.4109 224.973 21.3386L225.193 23.8045H223.911L223.716 21.5828C223.643 20.716 223.301 20.4231 222.239 20.4231H220.726V23.8045H219.444ZM220.726 19.2267H222.117C223.179 19.2267 223.777 18.7018 223.777 17.7862C223.777 16.8585 223.179 16.3458 222.117 16.3458H220.726V19.2267Z" fill="#fff"/>
|
||||
<path d="M214.957 23.9999C213.09 23.9999 211.748 22.4251 211.748 19.4831C211.748 16.6876 212.956 14.9419 214.994 14.9419C216.63 14.9419 217.704 16.0772 218.082 17.8107L216.752 17.8961C216.508 16.8341 215.934 16.1504 215.006 16.1504C213.774 16.1504 213.078 17.3468 213.078 19.4831C213.078 21.6072 213.798 22.7913 215.067 22.7913C216.178 22.7913 216.861 21.8758 216.91 20.4963H215.006V19.3854H218.094V23.8045H217.057L217.02 22.6937C216.642 23.4749 215.983 23.9999 214.957 23.9999Z" fill="#fff"/>
|
||||
<path d="M207.651 23.9999C205.589 23.9999 204.429 22.3763 204.429 19.4831C204.429 16.5655 205.589 14.9419 207.651 14.9419C209.714 14.9419 210.873 16.5655 210.873 19.4831C210.873 22.3763 209.714 23.9999 207.651 23.9999ZM207.651 22.8035C208.872 22.8035 209.543 21.6804 209.543 19.4831C209.543 17.2857 208.872 16.1382 207.651 16.1382C206.431 16.1382 205.76 17.2857 205.76 19.4831C205.76 21.6804 206.431 22.8035 207.651 22.8035Z" fill="#fff"/>
|
||||
<path d="M197.489 23.8045V15.1372H200.187C202.017 15.1372 203.14 16.126 203.14 17.7252C203.14 18.7628 202.481 19.5197 201.676 19.7761C202.53 19.9348 202.945 20.4109 203.018 21.3386L203.238 23.8045H201.956L201.761 21.5828C201.688 20.716 201.346 20.4231 200.284 20.4231H198.771V23.8045H197.489ZM198.771 19.2267H200.162C201.224 19.2267 201.822 18.7018 201.822 17.7862C201.822 16.8585 201.224 16.3458 200.162 16.3458H198.771V19.2267Z" fill="#fff"/>
|
||||
<path d="M190.403 23.8045V15.1372H193.088C194.894 15.1372 196.09 16.1626 196.09 17.8351C196.09 19.4953 194.894 20.5207 193.088 20.5207H191.684V23.8045H190.403ZM191.684 19.3122H192.954C194.186 19.3122 194.76 18.7995 194.76 17.8351C194.76 16.8707 194.186 16.3458 192.954 16.3458H191.684V19.3122Z" fill="#fff"/>
|
||||
<path d="M175.791 23.8045V15.1372H181.112V16.3458H177.073V18.8727H180.966V20.0446H177.073V22.596H181.21V23.8045H175.791Z" fill="#fff"/>
|
||||
<path d="M168.216 23.8045V15.1372H170.914C172.744 15.1372 173.867 16.126 173.867 17.7252C173.867 18.7628 173.208 19.5197 172.403 19.7761C173.257 19.9348 173.672 20.4109 173.745 21.3386L173.965 23.8045H172.683L172.488 21.5828C172.415 20.716 172.073 20.4231 171.011 20.4231H169.498V23.8045H168.216ZM169.498 19.2267H170.889C171.951 19.2267 172.549 18.7018 172.549 17.7862C172.549 16.8585 171.951 16.3458 170.889 16.3458H169.498V19.2267Z" fill="#fff"/>
|
||||
<path d="M160.374 23.8045L162.912 15.1372H164.572L167.111 23.8045H165.78L165.158 21.5462H162.326L161.704 23.8045H160.374ZM163.742 16.4678L162.656 20.362H164.828L163.742 16.4678Z" fill="#fff"/>
|
||||
<path d="M154.032 23.8045L153.104 15.1372H154.386L155.045 22.1565L155.862 16.1138H156.985L157.803 22.1565L158.462 15.1372H159.744L158.816 23.8045H157.144L156.424 18.5309L155.704 23.8045H154.032Z" fill="#fff"/>
|
||||
<path d="M148.471 23.8045V16.3458H145.932V15.1372H152.279V16.3458H149.74V23.8045H148.471Z" fill="#fff"/>
|
||||
<path d="M139.322 23.8045V15.1372H144.741V16.3213H140.604V18.9826H144.522V20.1545H140.604V23.8045H139.322Z" fill="#fff"/>
|
||||
<path d="M134.469 23.9999C132.407 23.9999 131.247 22.3763 131.247 19.4831C131.247 16.5655 132.407 14.9419 134.469 14.9419C136.532 14.9419 137.691 16.5655 137.691 19.4831C137.691 22.3763 136.532 23.9999 134.469 23.9999ZM134.469 22.8035C135.69 22.8035 136.361 21.6804 136.361 19.4831C136.361 17.2857 135.69 16.1382 134.469 16.1382C133.249 16.1382 132.577 17.2857 132.577 19.4831C132.577 21.6804 133.249 22.8035 134.469 22.8035Z" fill="#fff"/>
|
||||
<path d="M127.261 23.9999C125.381 23.9999 124.161 22.8035 124.014 20.9846L125.332 20.8992C125.503 22.1443 126.211 22.8035 127.31 22.8035C128.371 22.8035 128.969 22.3763 128.969 21.6072C128.969 20.8381 128.554 20.3987 126.736 19.8859C124.954 19.3976 124.209 18.7995 124.209 17.4566C124.209 15.9673 125.308 14.9419 127.126 14.9419C128.933 14.9419 129.97 16.0772 130.141 17.7496L128.823 17.8229C128.689 16.8219 128.127 16.1382 127.09 16.1382C126.138 16.1382 125.528 16.6387 125.528 17.3956C125.528 18.1525 125.955 18.4332 127.493 18.8605C129.567 19.4221 130.288 20.1911 130.288 21.6072C130.288 23.0355 129.079 23.9999 127.261 23.9999Z" fill="#fff"/>
|
||||
<path d="M109.927 23.8045V15.1372H115.248V16.3458H111.208V18.8727H115.102V20.0446H111.208V22.596H115.346V23.8045H109.927Z" fill="#fff"/>
|
||||
<path d="M105.232 23.9999C103.145 23.9999 101.974 22.2664 101.974 19.4831C101.974 16.6876 103.145 14.9419 105.232 14.9419C106.807 14.9419 107.942 16.0284 108.296 17.9083L106.953 17.9816C106.709 16.773 106.05 16.1382 105.232 16.1382C103.975 16.1382 103.304 17.3468 103.304 19.4831C103.304 21.6072 103.975 22.8035 105.232 22.8035C106.123 22.8035 106.795 22.1077 107.014 20.7893L108.345 20.8503C108.04 22.8279 106.892 23.9999 105.232 23.9999Z" fill="#fff"/>
|
||||
<path d="M95.0342 23.8045V15.1372H97.7315C99.5622 15.1372 100.685 16.126 100.685 17.7252C100.685 18.7628 100.026 19.5197 99.2205 19.7761C100.075 19.9348 100.49 20.4109 100.563 21.3386L100.783 23.8045H99.5012L99.3059 21.5828C99.2327 20.716 98.891 20.4231 97.8291 20.4231H96.3157V23.8045H95.0342ZM96.3157 19.2267H97.7071C98.7689 19.2267 99.367 18.7018 99.367 17.7862C99.367 16.8585 98.7689 16.3458 97.7071 16.3458H96.3157V19.2267Z" fill="#fff"/>
|
||||
<path d="M90.5596 23.9999C88.7166 23.9999 87.606 22.8035 87.606 20.7893V15.1372H88.8875V20.8381C88.8875 22.1077 89.4733 22.8035 90.5596 22.8035C91.6458 22.8035 92.2317 22.1077 92.2317 20.8381V15.1372H93.5132V20.7893C93.5132 22.8035 92.4025 23.9999 90.5596 23.9999Z" fill="#fff"/>
|
||||
<path d="M83.2417 23.9999C81.179 23.9999 80.0195 22.3763 80.0195 19.4831C80.0195 16.5655 81.179 14.9419 83.2417 14.9419C85.3043 14.9419 86.4638 16.5655 86.4638 19.4831C86.4638 22.3763 85.3043 23.9999 83.2417 23.9999ZM83.2417 22.8035C84.4622 22.8035 85.1334 21.6804 85.1334 19.4831C85.1334 17.2857 84.4622 16.1382 83.2417 16.1382C82.0212 16.1382 81.3499 17.2857 81.3499 19.4831C81.3499 21.6804 82.0212 22.8035 83.2417 22.8035Z" fill="#fff"/>
|
||||
<path d="M76.0332 23.9999C74.1536 23.9999 72.9331 22.8035 72.7866 20.9846L74.1048 20.8992C74.2756 22.1443 74.9835 22.8035 76.082 22.8035C77.1438 22.8035 77.7419 22.3763 77.7419 21.6072C77.7419 20.8381 77.3269 20.3987 75.5083 19.8859C73.7264 19.3976 72.9819 18.7995 72.9819 17.4566C72.9819 15.9673 74.0804 14.9419 75.8989 14.9419C77.7052 14.9419 78.7427 16.0772 78.9135 17.7496L77.5954 17.8229C77.4611 16.8219 76.8997 16.1382 75.8623 16.1382C74.9103 16.1382 74.3 16.6387 74.3 17.3956C74.3 18.1525 74.7272 18.4332 76.265 18.8605C78.3399 19.4221 79.06 20.1911 79.06 21.6072C79.06 23.0355 77.8517 23.9999 76.0332 23.9999Z" fill="#fff"/>
|
||||
<path d="M58.4556 23.8045V15.1372H60.0178L62.8616 21.9856V15.1372H64.1187V23.8045H62.5564L59.7127 16.9561V23.8045H58.4556Z" fill="#fff"/>
|
||||
<path d="M51.3813 23.8045V15.1372H56.7027V16.3458H52.6629V18.8727H56.5563V20.0446H52.6629V22.596H56.8004V23.8045H51.3813Z" fill="#fff"/>
|
||||
<path d="M44.0386 23.8045V15.1372H46.7237C48.53 15.1372 49.7261 16.1626 49.7261 17.8351C49.7261 19.4953 48.53 20.5207 46.7237 20.5207H45.3201V23.8045H44.0386ZM45.3201 19.3122H46.5894C47.8221 19.3122 48.3958 18.7995 48.3958 17.8351C48.3958 16.8707 47.8221 16.3458 46.5894 16.3458H45.3201V19.3122Z" fill="#fff"/>
|
||||
<path d="M39.332 23.9999C37.2693 23.9999 36.1099 22.3763 36.1099 19.4831C36.1099 16.5655 37.2693 14.9419 39.332 14.9419C41.3946 14.9419 42.5541 16.5655 42.5541 19.4831C42.5541 22.3763 41.3946 23.9999 39.332 23.9999ZM39.332 22.8035C40.5525 22.8035 41.2238 21.6804 41.2238 19.4831C41.2238 17.2857 40.5525 16.1382 39.332 16.1382C38.1115 16.1382 37.4402 17.2857 37.4402 19.4831C37.4402 21.6804 38.1115 22.8035 39.332 22.8035Z" fill="#fff"/>
|
||||
<path d="M171.085 9.35094C169.083 9.35094 167.96 7.95929 167.96 5.46895C167.96 2.60019 168.864 0.292969 171.414 0.292969C173.013 0.292969 173.819 1.1597 174.148 2.38045L172.83 2.49032C172.659 1.89215 172.269 1.48931 171.414 1.48931C170.243 1.48931 169.474 2.36825 169.303 4.2482C169.718 3.71107 170.426 3.32043 171.329 3.32043C173.05 3.32043 174.161 4.48015 174.161 6.25024C174.161 8.16681 172.916 9.35094 171.085 9.35094ZM171.085 8.1424C172.147 8.1424 172.83 7.45878 172.83 6.27465C172.83 5.17597 172.196 4.48015 171.183 4.48015C170.096 4.48015 169.315 5.17597 169.315 6.27465C169.315 7.34891 169.999 8.1424 171.085 8.1424Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M160.667 9.15562C160.667 7.22684 161.387 6.01829 163.925 4.67547C165.011 4.10171 165.475 3.72328 165.475 2.88096C165.475 2.05085 164.95 1.48931 163.937 1.48931C162.827 1.48931 162.217 2.09968 162.046 3.18615L160.715 3.1007C160.898 1.37944 162.021 0.292969 163.937 0.292969C165.78 0.292969 166.806 1.3184 166.806 2.83213C166.806 4.17496 166.22 4.83416 164.45 5.78635C162.949 6.59205 162.241 7.36112 162.192 7.94708H166.818V9.15562H160.667Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M156.424 9.35094C154.508 9.35094 153.348 7.61748 153.348 4.83416C153.348 2.03864 154.508 0.292969 156.424 0.292969C158.34 0.292969 159.499 2.03864 159.499 4.83416C159.499 7.61748 158.34 9.35094 156.424 9.35094ZM154.678 4.83416C154.678 5.55441 154.752 6.1892 154.91 6.68971L157.449 1.98981C157.181 1.66021 156.827 1.48931 156.424 1.48931C155.338 1.48931 154.678 2.72226 154.678 4.83416ZM156.424 8.15461C157.51 8.15461 158.169 6.92165 158.169 4.83416C158.169 4.10171 158.096 3.46692 157.937 2.96641L155.386 7.6541C155.667 7.9837 156.021 8.15461 156.424 8.15461Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M146.03 9.15562C146.03 7.22684 146.75 6.01829 149.289 4.67547C150.375 4.10171 150.839 3.72328 150.839 2.88096C150.839 2.05085 150.314 1.48931 149.301 1.48931C148.19 1.48931 147.58 2.09968 147.409 3.18615L146.079 3.1007C146.262 1.37944 147.385 0.292969 149.301 0.292969C151.144 0.292969 152.169 1.3184 152.169 2.83213C152.169 4.17496 151.583 4.83416 149.813 5.78635C148.312 6.59205 147.604 7.36112 147.555 7.94708H152.181V9.15562H146.03Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M132.041 10.4985L135.727 0H136.899L133.212 10.4985H132.041Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M124.722 10.4985L128.409 0H129.581L125.894 10.4985H124.722Z" fill="#fff" fill-opacity="0.5"/>
|
||||
<path d="M111.684 9.15582V7.54443H113.344V9.15582H111.684Z" fill="#fff"/>
|
||||
<path d="M105.232 9.35094C103.145 9.35094 101.974 7.61748 101.974 4.83416C101.974 2.03864 103.145 0.292969 105.232 0.292969C106.807 0.292969 107.942 1.37944 108.296 3.25939L106.953 3.33264C106.709 2.1241 106.05 1.48931 105.232 1.48931C103.975 1.48931 103.304 2.69785 103.304 4.83416C103.304 6.95827 103.975 8.15461 105.232 8.15461C106.123 8.15461 106.795 7.45878 107.014 6.14037L108.345 6.20141C108.04 8.17902 106.892 9.35094 105.232 9.35094Z" fill="#fff"/>
|
||||
<path d="M95.0464 9.15562V0.488281H96.6086L99.4524 7.3367V0.488281H100.71V9.15562H99.1473L96.3035 2.3072V9.15562H95.0464Z" fill="#fff"/>
|
||||
<path d="M87.8501 9.15562V7.94707H89.9249V1.69683H87.8501V0.488281H93.2691V1.69683H91.1943V7.94707H93.2691V9.15562H87.8501Z" fill="#fff"/>
|
||||
<path d="M73.4575 9.15562V0.488281H74.739V7.94707H78.8765V9.15562H73.4575Z" fill="#fff"/>
|
||||
<path d="M66.0171 9.15562V0.488281H71.3385V1.69683H67.2986V4.22378H71.192V5.3957H67.2986V7.94707H71.4361V9.15562H66.0171Z" fill="#fff"/>
|
||||
<path d="M61.3232 9.35094C59.2361 9.35094 58.0645 7.61748 58.0645 4.83416C58.0645 2.03864 59.2361 0.292969 61.3232 0.292969C62.8976 0.292969 64.0327 1.37944 64.3866 3.25939L63.0441 3.33264C62.8 2.1241 62.1409 1.48931 61.3232 1.48931C60.0661 1.48931 59.3948 2.69785 59.3948 4.83416C59.3948 6.95827 60.0661 8.15461 61.3232 8.15461C62.2142 8.15461 62.8854 7.45878 63.1051 6.14037L64.4355 6.20141C64.1303 8.17902 62.9831 9.35094 61.3232 9.35094Z" fill="#fff"/>
|
||||
<path d="M51.125 9.15562V0.488281H53.8223C55.6531 0.488281 56.7759 1.47709 56.7759 3.07627C56.7759 4.11391 56.1168 4.87078 55.3113 5.12714C56.1657 5.28583 56.5806 5.76193 56.6539 6.6897L56.8736 9.15562H55.592L55.3968 6.93385C55.3235 6.06711 54.9818 5.77413 53.9199 5.77413H52.4065V9.15562H51.125ZM52.4065 4.5778H53.7979C54.8597 4.5778 55.4578 4.05288 55.4578 3.13731C55.4578 2.20954 54.8597 1.69683 53.7979 1.69683H52.4065V4.5778Z" fill="#fff"/>
|
||||
<path d="M44.063 9.15562V0.488281H49.3844V1.69683H45.3445V4.22378H49.2379V5.3957H45.3445V7.94707H49.482V9.15562H44.063Z" fill="#fff"/>
|
||||
<path d="M38.5753 9.15562L36 0.488281H37.3548L39.332 7.44656L41.3092 0.488281H42.6639L40.0887 9.15562H38.5753Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,44 @@
|
||||
<svg width="240" height="24" viewBox="0 0 240 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.4283 0.610352L26.8565 23.8046H0L13.4283 0.610352Z" fill="#000"/>
|
||||
<path d="M236.436 22.7547L235.081 17.1148V23.8045H233.849V15.1372H235.557L236.924 20.7405L238.291 15.1372H240V23.8045H238.767V17.1148L237.412 22.7547H236.436Z" fill="#000"/>
|
||||
<path d="M226.237 23.8045L228.776 15.1372H230.436L232.974 23.8045H231.644L231.022 21.5462H228.19L227.568 23.8045H226.237ZM229.606 16.4678L228.52 20.362H230.692L229.606 16.4678Z" fill="#000"/>
|
||||
<path d="M219.444 23.8045V15.1372H222.142C223.972 15.1372 225.095 16.126 225.095 17.7252C225.095 18.7628 224.436 19.5197 223.631 19.7761C224.485 19.9348 224.9 20.4109 224.973 21.3386L225.193 23.8045H223.911L223.716 21.5828C223.643 20.716 223.301 20.4231 222.239 20.4231H220.726V23.8045H219.444ZM220.726 19.2267H222.117C223.179 19.2267 223.777 18.7018 223.777 17.7862C223.777 16.8585 223.179 16.3458 222.117 16.3458H220.726V19.2267Z" fill="#000"/>
|
||||
<path d="M214.957 23.9999C213.09 23.9999 211.748 22.4251 211.748 19.4831C211.748 16.6876 212.956 14.9419 214.994 14.9419C216.63 14.9419 217.704 16.0772 218.082 17.8107L216.752 17.8961C216.508 16.8341 215.934 16.1504 215.006 16.1504C213.774 16.1504 213.078 17.3468 213.078 19.4831C213.078 21.6072 213.798 22.7913 215.067 22.7913C216.178 22.7913 216.861 21.8758 216.91 20.4963H215.006V19.3854H218.094V23.8045H217.057L217.02 22.6937C216.642 23.4749 215.983 23.9999 214.957 23.9999Z" fill="#000"/>
|
||||
<path d="M207.651 23.9999C205.589 23.9999 204.429 22.3763 204.429 19.4831C204.429 16.5655 205.589 14.9419 207.651 14.9419C209.714 14.9419 210.873 16.5655 210.873 19.4831C210.873 22.3763 209.714 23.9999 207.651 23.9999ZM207.651 22.8035C208.872 22.8035 209.543 21.6804 209.543 19.4831C209.543 17.2857 208.872 16.1382 207.651 16.1382C206.431 16.1382 205.76 17.2857 205.76 19.4831C205.76 21.6804 206.431 22.8035 207.651 22.8035Z" fill="#000"/>
|
||||
<path d="M197.489 23.8045V15.1372H200.187C202.017 15.1372 203.14 16.126 203.14 17.7252C203.14 18.7628 202.481 19.5197 201.676 19.7761C202.53 19.9348 202.945 20.4109 203.018 21.3386L203.238 23.8045H201.956L201.761 21.5828C201.688 20.716 201.346 20.4231 200.284 20.4231H198.771V23.8045H197.489ZM198.771 19.2267H200.162C201.224 19.2267 201.822 18.7018 201.822 17.7862C201.822 16.8585 201.224 16.3458 200.162 16.3458H198.771V19.2267Z" fill="#000"/>
|
||||
<path d="M190.403 23.8045V15.1372H193.088C194.894 15.1372 196.09 16.1626 196.09 17.8351C196.09 19.4953 194.894 20.5207 193.088 20.5207H191.684V23.8045H190.403ZM191.684 19.3122H192.954C194.186 19.3122 194.76 18.7995 194.76 17.8351C194.76 16.8707 194.186 16.3458 192.954 16.3458H191.684V19.3122Z" fill="#000"/>
|
||||
<path d="M175.791 23.8045V15.1372H181.112V16.3458H177.073V18.8727H180.966V20.0446H177.073V22.596H181.21V23.8045H175.791Z" fill="#000"/>
|
||||
<path d="M168.216 23.8045V15.1372H170.914C172.744 15.1372 173.867 16.126 173.867 17.7252C173.867 18.7628 173.208 19.5197 172.403 19.7761C173.257 19.9348 173.672 20.4109 173.745 21.3386L173.965 23.8045H172.683L172.488 21.5828C172.415 20.716 172.073 20.4231 171.011 20.4231H169.498V23.8045H168.216ZM169.498 19.2267H170.889C171.951 19.2267 172.549 18.7018 172.549 17.7862C172.549 16.8585 171.951 16.3458 170.889 16.3458H169.498V19.2267Z" fill="#000"/>
|
||||
<path d="M160.374 23.8045L162.912 15.1372H164.572L167.111 23.8045H165.78L165.158 21.5462H162.326L161.704 23.8045H160.374ZM163.742 16.4678L162.656 20.362H164.828L163.742 16.4678Z" fill="#000"/>
|
||||
<path d="M154.032 23.8045L153.104 15.1372H154.386L155.045 22.1565L155.862 16.1138H156.985L157.803 22.1565L158.462 15.1372H159.744L158.816 23.8045H157.144L156.424 18.5309L155.704 23.8045H154.032Z" fill="#000"/>
|
||||
<path d="M148.471 23.8045V16.3458H145.932V15.1372H152.279V16.3458H149.74V23.8045H148.471Z" fill="#000"/>
|
||||
<path d="M139.322 23.8045V15.1372H144.741V16.3213H140.604V18.9826H144.522V20.1545H140.604V23.8045H139.322Z" fill="#000"/>
|
||||
<path d="M134.469 23.9999C132.407 23.9999 131.247 22.3763 131.247 19.4831C131.247 16.5655 132.407 14.9419 134.469 14.9419C136.532 14.9419 137.691 16.5655 137.691 19.4831C137.691 22.3763 136.532 23.9999 134.469 23.9999ZM134.469 22.8035C135.69 22.8035 136.361 21.6804 136.361 19.4831C136.361 17.2857 135.69 16.1382 134.469 16.1382C133.249 16.1382 132.577 17.2857 132.577 19.4831C132.577 21.6804 133.249 22.8035 134.469 22.8035Z" fill="#000"/>
|
||||
<path d="M127.261 23.9999C125.381 23.9999 124.161 22.8035 124.014 20.9846L125.332 20.8992C125.503 22.1443 126.211 22.8035 127.31 22.8035C128.371 22.8035 128.969 22.3763 128.969 21.6072C128.969 20.8381 128.554 20.3987 126.736 19.8859C124.954 19.3976 124.209 18.7995 124.209 17.4566C124.209 15.9673 125.308 14.9419 127.126 14.9419C128.933 14.9419 129.97 16.0772 130.141 17.7496L128.823 17.8229C128.689 16.8219 128.127 16.1382 127.09 16.1382C126.138 16.1382 125.528 16.6387 125.528 17.3956C125.528 18.1525 125.955 18.4332 127.493 18.8605C129.567 19.4221 130.288 20.1911 130.288 21.6072C130.288 23.0355 129.079 23.9999 127.261 23.9999Z" fill="#000"/>
|
||||
<path d="M109.927 23.8045V15.1372H115.248V16.3458H111.208V18.8727H115.102V20.0446H111.208V22.596H115.346V23.8045H109.927Z" fill="#000"/>
|
||||
<path d="M105.232 23.9999C103.145 23.9999 101.974 22.2664 101.974 19.4831C101.974 16.6876 103.145 14.9419 105.232 14.9419C106.807 14.9419 107.942 16.0284 108.296 17.9083L106.953 17.9816C106.709 16.773 106.05 16.1382 105.232 16.1382C103.975 16.1382 103.304 17.3468 103.304 19.4831C103.304 21.6072 103.975 22.8035 105.232 22.8035C106.123 22.8035 106.795 22.1077 107.014 20.7893L108.345 20.8503C108.04 22.8279 106.892 23.9999 105.232 23.9999Z" fill="#000"/>
|
||||
<path d="M95.0342 23.8045V15.1372H97.7315C99.5622 15.1372 100.685 16.126 100.685 17.7252C100.685 18.7628 100.026 19.5197 99.2205 19.7761C100.075 19.9348 100.49 20.4109 100.563 21.3386L100.783 23.8045H99.5012L99.3059 21.5828C99.2327 20.716 98.891 20.4231 97.8291 20.4231H96.3157V23.8045H95.0342ZM96.3157 19.2267H97.7071C98.7689 19.2267 99.367 18.7018 99.367 17.7862C99.367 16.8585 98.7689 16.3458 97.7071 16.3458H96.3157V19.2267Z" fill="#000"/>
|
||||
<path d="M90.5596 23.9999C88.7166 23.9999 87.606 22.8035 87.606 20.7893V15.1372H88.8875V20.8381C88.8875 22.1077 89.4733 22.8035 90.5596 22.8035C91.6458 22.8035 92.2317 22.1077 92.2317 20.8381V15.1372H93.5132V20.7893C93.5132 22.8035 92.4025 23.9999 90.5596 23.9999Z" fill="#000"/>
|
||||
<path d="M83.2417 23.9999C81.179 23.9999 80.0195 22.3763 80.0195 19.4831C80.0195 16.5655 81.179 14.9419 83.2417 14.9419C85.3043 14.9419 86.4638 16.5655 86.4638 19.4831C86.4638 22.3763 85.3043 23.9999 83.2417 23.9999ZM83.2417 22.8035C84.4622 22.8035 85.1334 21.6804 85.1334 19.4831C85.1334 17.2857 84.4622 16.1382 83.2417 16.1382C82.0212 16.1382 81.3499 17.2857 81.3499 19.4831C81.3499 21.6804 82.0212 22.8035 83.2417 22.8035Z" fill="#000"/>
|
||||
<path d="M76.0332 23.9999C74.1536 23.9999 72.9331 22.8035 72.7866 20.9846L74.1048 20.8992C74.2756 22.1443 74.9835 22.8035 76.082 22.8035C77.1438 22.8035 77.7419 22.3763 77.7419 21.6072C77.7419 20.8381 77.3269 20.3987 75.5083 19.8859C73.7264 19.3976 72.9819 18.7995 72.9819 17.4566C72.9819 15.9673 74.0804 14.9419 75.8989 14.9419C77.7052 14.9419 78.7427 16.0772 78.9135 17.7496L77.5954 17.8229C77.4611 16.8219 76.8997 16.1382 75.8623 16.1382C74.9103 16.1382 74.3 16.6387 74.3 17.3956C74.3 18.1525 74.7272 18.4332 76.265 18.8605C78.3399 19.4221 79.06 20.1911 79.06 21.6072C79.06 23.0355 77.8517 23.9999 76.0332 23.9999Z" fill="#000"/>
|
||||
<path d="M58.4556 23.8045V15.1372H60.0178L62.8616 21.9856V15.1372H64.1187V23.8045H62.5564L59.7127 16.9561V23.8045H58.4556Z" fill="#000"/>
|
||||
<path d="M51.3813 23.8045V15.1372H56.7027V16.3458H52.6629V18.8727H56.5563V20.0446H52.6629V22.596H56.8004V23.8045H51.3813Z" fill="#000"/>
|
||||
<path d="M44.0386 23.8045V15.1372H46.7237C48.53 15.1372 49.7261 16.1626 49.7261 17.8351C49.7261 19.4953 48.53 20.5207 46.7237 20.5207H45.3201V23.8045H44.0386ZM45.3201 19.3122H46.5894C47.8221 19.3122 48.3958 18.7995 48.3958 17.8351C48.3958 16.8707 47.8221 16.3458 46.5894 16.3458H45.3201V19.3122Z" fill="#000"/>
|
||||
<path d="M39.332 23.9999C37.2693 23.9999 36.1099 22.3763 36.1099 19.4831C36.1099 16.5655 37.2693 14.9419 39.332 14.9419C41.3946 14.9419 42.5541 16.5655 42.5541 19.4831C42.5541 22.3763 41.3946 23.9999 39.332 23.9999ZM39.332 22.8035C40.5525 22.8035 41.2238 21.6804 41.2238 19.4831C41.2238 17.2857 40.5525 16.1382 39.332 16.1382C38.1115 16.1382 37.4402 17.2857 37.4402 19.4831C37.4402 21.6804 38.1115 22.8035 39.332 22.8035Z" fill="#000"/>
|
||||
<path d="M171.085 9.35094C169.083 9.35094 167.96 7.95929 167.96 5.46895C167.96 2.60019 168.864 0.292969 171.414 0.292969C173.013 0.292969 173.819 1.1597 174.148 2.38045L172.83 2.49032C172.659 1.89215 172.269 1.48931 171.414 1.48931C170.243 1.48931 169.474 2.36825 169.303 4.2482C169.718 3.71107 170.426 3.32043 171.329 3.32043C173.05 3.32043 174.161 4.48015 174.161 6.25024C174.161 8.16681 172.916 9.35094 171.085 9.35094ZM171.085 8.1424C172.147 8.1424 172.83 7.45878 172.83 6.27465C172.83 5.17597 172.196 4.48015 171.183 4.48015C170.096 4.48015 169.315 5.17597 169.315 6.27465C169.315 7.34891 169.999 8.1424 171.085 8.1424Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M160.667 9.15562C160.667 7.22684 161.387 6.01829 163.925 4.67547C165.011 4.10171 165.475 3.72328 165.475 2.88096C165.475 2.05085 164.95 1.48931 163.937 1.48931C162.827 1.48931 162.217 2.09968 162.046 3.18615L160.715 3.1007C160.898 1.37944 162.021 0.292969 163.937 0.292969C165.78 0.292969 166.806 1.3184 166.806 2.83213C166.806 4.17496 166.22 4.83416 164.45 5.78635C162.949 6.59205 162.241 7.36112 162.192 7.94708H166.818V9.15562H160.667Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M156.424 9.35094C154.508 9.35094 153.348 7.61748 153.348 4.83416C153.348 2.03864 154.508 0.292969 156.424 0.292969C158.34 0.292969 159.499 2.03864 159.499 4.83416C159.499 7.61748 158.34 9.35094 156.424 9.35094ZM154.678 4.83416C154.678 5.55441 154.752 6.1892 154.91 6.68971L157.449 1.98981C157.181 1.66021 156.827 1.48931 156.424 1.48931C155.338 1.48931 154.678 2.72226 154.678 4.83416ZM156.424 8.15461C157.51 8.15461 158.169 6.92165 158.169 4.83416C158.169 4.10171 158.096 3.46692 157.937 2.96641L155.386 7.6541C155.667 7.9837 156.021 8.15461 156.424 8.15461Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M146.03 9.15562C146.03 7.22684 146.75 6.01829 149.289 4.67547C150.375 4.10171 150.839 3.72328 150.839 2.88096C150.839 2.05085 150.314 1.48931 149.301 1.48931C148.19 1.48931 147.58 2.09968 147.409 3.18615L146.079 3.1007C146.262 1.37944 147.385 0.292969 149.301 0.292969C151.144 0.292969 152.169 1.3184 152.169 2.83213C152.169 4.17496 151.583 4.83416 149.813 5.78635C148.312 6.59205 147.604 7.36112 147.555 7.94708H152.181V9.15562H146.03Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M132.041 10.4985L135.727 0H136.899L133.212 10.4985H132.041Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M124.722 10.4985L128.409 0H129.581L125.894 10.4985H124.722Z" fill="#000" fill-opacity="0.5"/>
|
||||
<path d="M111.684 9.15582V7.54443H113.344V9.15582H111.684Z" fill="#000"/>
|
||||
<path d="M105.232 9.35094C103.145 9.35094 101.974 7.61748 101.974 4.83416C101.974 2.03864 103.145 0.292969 105.232 0.292969C106.807 0.292969 107.942 1.37944 108.296 3.25939L106.953 3.33264C106.709 2.1241 106.05 1.48931 105.232 1.48931C103.975 1.48931 103.304 2.69785 103.304 4.83416C103.304 6.95827 103.975 8.15461 105.232 8.15461C106.123 8.15461 106.795 7.45878 107.014 6.14037L108.345 6.20141C108.04 8.17902 106.892 9.35094 105.232 9.35094Z" fill="#000"/>
|
||||
<path d="M95.0464 9.15562V0.488281H96.6086L99.4524 7.3367V0.488281H100.71V9.15562H99.1473L96.3035 2.3072V9.15562H95.0464Z" fill="#000"/>
|
||||
<path d="M87.8501 9.15562V7.94707H89.9249V1.69683H87.8501V0.488281H93.2691V1.69683H91.1943V7.94707H93.2691V9.15562H87.8501Z" fill="#000"/>
|
||||
<path d="M73.4575 9.15562V0.488281H74.739V7.94707H78.8765V9.15562H73.4575Z" fill="#000"/>
|
||||
<path d="M66.0171 9.15562V0.488281H71.3385V1.69683H67.2986V4.22378H71.192V5.3957H67.2986V7.94707H71.4361V9.15562H66.0171Z" fill="#000"/>
|
||||
<path d="M61.3232 9.35094C59.2361 9.35094 58.0645 7.61748 58.0645 4.83416C58.0645 2.03864 59.2361 0.292969 61.3232 0.292969C62.8976 0.292969 64.0327 1.37944 64.3866 3.25939L63.0441 3.33264C62.8 2.1241 62.1409 1.48931 61.3232 1.48931C60.0661 1.48931 59.3948 2.69785 59.3948 4.83416C59.3948 6.95827 60.0661 8.15461 61.3232 8.15461C62.2142 8.15461 62.8854 7.45878 63.1051 6.14037L64.4355 6.20141C64.1303 8.17902 62.9831 9.35094 61.3232 9.35094Z" fill="#000"/>
|
||||
<path d="M51.125 9.15562V0.488281H53.8223C55.6531 0.488281 56.7759 1.47709 56.7759 3.07627C56.7759 4.11391 56.1168 4.87078 55.3113 5.12714C56.1657 5.28583 56.5806 5.76193 56.6539 6.6897L56.8736 9.15562H55.592L55.3968 6.93385C55.3235 6.06711 54.9818 5.77413 53.9199 5.77413H52.4065V9.15562H51.125ZM52.4065 4.5778H53.7979C54.8597 4.5778 55.4578 4.05288 55.4578 3.13731C55.4578 2.20954 54.8597 1.69683 53.7979 1.69683H52.4065V4.5778Z" fill="#000"/>
|
||||
<path d="M44.063 9.15562V0.488281H49.3844V1.69683H45.3445V4.22378H49.2379V5.3957H45.3445V7.94707H49.482V9.15562H44.063Z" fill="#000"/>
|
||||
<path d="M38.5753 9.15562L36 0.488281H37.3548L39.332 7.44656L41.3092 0.488281H42.6639L40.0887 9.15562H38.5753Z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -39,377 +39,288 @@ python3 << 'PYTHON_SCRIPT'
|
||||
import csv
|
||||
import subprocess
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
import re
|
||||
|
||||
# Increase CSV field size limit to handle large prompt content
|
||||
csv.field_size_limit(sys.maxsize)
|
||||
|
||||
project_dir = os.environ.get('PROJECT_DIR', '.')
|
||||
csv_file = os.path.join(project_dir, 'prompts.csv')
|
||||
remote_csv = os.path.join(project_dir, 'prompts.csv.remote')
|
||||
prompts_md_path = os.path.join(project_dir, 'PROMPTS.md')
|
||||
|
||||
# Read existing local prompts (by act title as key)
|
||||
local_prompts = {}
|
||||
fieldnames = None
|
||||
skipped_local = 0
|
||||
with open(csv_file, 'r', newline='', encoding='utf-8') as f:
|
||||
# Normalize CRLF to LF
|
||||
content = f.read().replace('\r\n', '\n').replace('\r', '\n')
|
||||
reader = csv.DictReader(io.StringIO(content))
|
||||
fieldnames = reader.fieldnames
|
||||
for row in reader:
|
||||
try:
|
||||
act = row.get('act', '').strip()
|
||||
if act:
|
||||
local_prompts[act] = row
|
||||
except csv.Error as e:
|
||||
skipped_local += 1
|
||||
print(f"Skipping local row due to CSV error: {e}")
|
||||
# --- CSV I/O (ordered, roundtrip-safe) ---
|
||||
|
||||
print(f"Found {len(local_prompts)} existing local prompts" + (f" (skipped {skipped_local})" if skipped_local else ""))
|
||||
def read_csv(path):
|
||||
"""Read CSV into an ordered list of rows and an act->index map."""
|
||||
rows = []
|
||||
index = {}
|
||||
skipped = 0
|
||||
with open(path, 'r', newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
fieldnames = list(reader.fieldnames) if reader.fieldnames else []
|
||||
while True:
|
||||
try:
|
||||
row = next(reader)
|
||||
act = (row.get('act') or '').strip()
|
||||
if act:
|
||||
index[act] = len(rows)
|
||||
rows.append(row)
|
||||
except csv.Error as e:
|
||||
skipped += 1
|
||||
print(f"Skipping row due to CSV error: {e}")
|
||||
except StopIteration:
|
||||
break
|
||||
return rows, index, fieldnames, skipped
|
||||
|
||||
# Read remote prompts (normalize CRLF to LF)
|
||||
remote_prompts = []
|
||||
skipped_remote = 0
|
||||
with open(remote_csv, 'r', newline='', encoding='utf-8') as f:
|
||||
content = f.read().replace('\r\n', '\n').replace('\r', '\n')
|
||||
reader = csv.DictReader(io.StringIO(content))
|
||||
remote_fieldnames = reader.fieldnames
|
||||
while True:
|
||||
try:
|
||||
row = next(reader)
|
||||
remote_prompts.append(row)
|
||||
except csv.Error as e:
|
||||
skipped_remote += 1
|
||||
print(f"Skipping remote row due to CSV error: {e}")
|
||||
except StopIteration:
|
||||
break
|
||||
def write_csv(rows, fieldnames, path):
|
||||
"""Write CSV deterministically — unchanged rows produce identical bytes."""
|
||||
with open(path, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for row in rows:
|
||||
writer.writerow(row)
|
||||
|
||||
print(f"Found {len(remote_prompts)} remote prompts" + (f" (skipped {skipped_remote})" if skipped_remote else ""))
|
||||
def rebuild_index(rows):
|
||||
"""Rebuild the act->index map after mutations."""
|
||||
return {(r.get('act') or '').strip(): i for i, r in enumerate(rows)}
|
||||
|
||||
# Use remote fieldnames if local is empty
|
||||
if not fieldnames:
|
||||
fieldnames = remote_fieldnames
|
||||
def normalize(s):
|
||||
"""Normalize a string for comparison only."""
|
||||
return (s or '').replace('\r\n', '\n').replace('\r', '\n').strip()
|
||||
|
||||
# Build set of remote prompt acts for quick lookup
|
||||
remote_acts = set()
|
||||
for row in remote_prompts:
|
||||
act = row.get('act', '').strip()
|
||||
if act:
|
||||
remote_acts.add(act)
|
||||
# --- Contributor helpers ---
|
||||
|
||||
# Find new, updated, and deleted prompts
|
||||
new_prompts = []
|
||||
updated_prompts = []
|
||||
deleted_prompts = []
|
||||
|
||||
# Check for new and updated
|
||||
for row in remote_prompts:
|
||||
act = row.get('act', '').strip()
|
||||
if not act:
|
||||
continue
|
||||
|
||||
if act not in local_prompts:
|
||||
new_prompts.append(row)
|
||||
else:
|
||||
local_row = local_prompts[act]
|
||||
# Check if content OR contributors changed
|
||||
content_changed = row.get('prompt', '').strip() != local_row.get('prompt', '').strip()
|
||||
contributors_changed = row.get('contributor', '').strip() != local_row.get('contributor', '').strip()
|
||||
if content_changed or contributors_changed:
|
||||
updated_prompts.append((row, local_row))
|
||||
|
||||
# Check for deleted (in local but not in remote)
|
||||
for act, local_row in local_prompts.items():
|
||||
if act not in remote_acts:
|
||||
deleted_prompts.append(local_row)
|
||||
|
||||
print(f"Found {len(new_prompts)} new prompts to add")
|
||||
print(f"Found {len(updated_prompts)} updated prompts to modify")
|
||||
print(f"Found {len(deleted_prompts)} prompts to remove (unlisted/deleted)")
|
||||
|
||||
# Helper function to parse contributors (supports "user1,user2,user3" format)
|
||||
def parse_contributors(contributor_field):
|
||||
"""Parse contributor field, returns (primary_author, co_authors_list)"""
|
||||
if not contributor_field:
|
||||
def parse_contributors(field):
|
||||
if not field:
|
||||
return 'anonymous', []
|
||||
|
||||
# Split by comma and clean up
|
||||
contributors = [c.strip() for c in contributor_field.split(',') if c.strip()]
|
||||
|
||||
if not contributors:
|
||||
parts = [c.strip() for c in field.split(',') if c.strip()]
|
||||
if not parts:
|
||||
return 'anonymous', []
|
||||
|
||||
primary = contributors[0]
|
||||
co_authors = contributors[1:] if len(contributors) > 1 else []
|
||||
return primary, co_authors
|
||||
return parts[0], parts[1:]
|
||||
|
||||
def build_commit_message(action, act, co_authors):
|
||||
"""Build commit message with optional co-author trailers"""
|
||||
def make_email(name):
|
||||
"""Build an email — don't double-suffix if the contributor is already an email."""
|
||||
if '@' in name:
|
||||
return name
|
||||
return f'{name}@users.noreply.github.com'
|
||||
|
||||
def build_commit_msg(action, act, co_authors):
|
||||
msg = f'{action} prompt: {act}'
|
||||
|
||||
if co_authors:
|
||||
msg += '\n\n'
|
||||
for co_author in co_authors:
|
||||
co_email = f"{co_author}@users.noreply.github.com"
|
||||
msg += f'Co-authored-by: {co_author} <{co_email}>\n'
|
||||
|
||||
for ca in co_authors:
|
||||
msg += f'Co-authored-by: {ca} <{make_email(ca)}>\n'
|
||||
return msg
|
||||
|
||||
def git_commit(author_name, author_email, message):
|
||||
"""Stage CSV + PROMPTS.md and commit if there are real changes. Returns True if committed."""
|
||||
subprocess.run(['git', 'add', csv_file, prompts_md_path], check=True)
|
||||
if subprocess.run(['git', 'diff', '--cached', '--quiet'], capture_output=True).returncode == 0:
|
||||
return False
|
||||
env = os.environ.copy()
|
||||
env['GIT_AUTHOR_NAME'] = author_name
|
||||
env['GIT_AUTHOR_EMAIL'] = author_email
|
||||
env['GIT_COMMITTER_NAME'] = author_name
|
||||
env['GIT_COMMITTER_EMAIL'] = author_email
|
||||
subprocess.run([
|
||||
'git', 'commit', '-m', message,
|
||||
f'--author={author_name} <{author_email}>'
|
||||
], env=env, check=True)
|
||||
return True
|
||||
|
||||
# --- PROMPTS.md helpers ---
|
||||
|
||||
EMAIL_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
||||
GITHUB_USERNAME_RE = re.compile(r'^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$')
|
||||
|
||||
def format_contributor_links(contributor_field):
|
||||
"""Format contributors as GitHub profile links"""
|
||||
if not contributor_field:
|
||||
return '@anonymous'
|
||||
|
||||
contributors = [c.strip() for c in contributor_field.split(',') if c.strip()]
|
||||
if not contributors:
|
||||
return '@anonymous'
|
||||
|
||||
return ', '.join([f'[@{c}](https://github.com/{c})' for c in contributors])
|
||||
|
||||
links = []
|
||||
for contributor in contributors:
|
||||
normalized = contributor.lstrip('@')
|
||||
if EMAIL_RE.match(normalized):
|
||||
links.append('@anonymous')
|
||||
elif GITHUB_USERNAME_RE.match(normalized):
|
||||
links.append(f'[@{normalized}](https://github.com/{normalized})')
|
||||
elif normalized:
|
||||
links.append(f'@{normalized}')
|
||||
return ', '.join(links) if links else '@anonymous'
|
||||
|
||||
def generate_prompt_block(row):
|
||||
"""Generate a single prompt's <details> block"""
|
||||
act = row.get('act', 'Untitled')
|
||||
prompt = row.get('prompt', '')
|
||||
contributor = row.get('contributor', '')
|
||||
prompt_type = row.get('type', 'TEXT').upper()
|
||||
|
||||
# Determine code block language based on type
|
||||
if prompt_type == 'TEXT':
|
||||
lang = 'md'
|
||||
elif prompt_type == 'JSON':
|
||||
lang = 'json'
|
||||
elif prompt_type == 'YAML':
|
||||
lang = 'yaml'
|
||||
else:
|
||||
lang = 'md'
|
||||
|
||||
contributor_links = format_contributor_links(contributor)
|
||||
|
||||
block = f'<details>\n'
|
||||
block += f'<summary><strong>{act}</strong></summary>\n\n'
|
||||
block += f'## {act}\n\n'
|
||||
block += f'Contributed by {contributor_links}\n\n'
|
||||
block += f'```{lang}\n'
|
||||
block += f'{prompt}\n'
|
||||
block += f'```\n\n'
|
||||
block += f'</details>\n\n'
|
||||
return block
|
||||
prompt_type = (row.get('type') or 'TEXT').upper()
|
||||
|
||||
def init_prompts_md(prompts_md_path):
|
||||
"""Initialize PROMPTS.md with header if it doesn't exist"""
|
||||
lang = {'TEXT': 'md', 'JSON': 'json', 'YAML': 'yaml'}.get(prompt_type, 'md')
|
||||
contributor_links = format_contributor_links(contributor)
|
||||
|
||||
return (
|
||||
f'<details>\n'
|
||||
f'<summary><strong>{act}</strong></summary>\n\n'
|
||||
f'## {act}\n\n'
|
||||
f'Contributed by {contributor_links}\n\n'
|
||||
f'```{lang}\n'
|
||||
f'{prompt}\n'
|
||||
f'```\n\n'
|
||||
f'</details>\n\n'
|
||||
)
|
||||
|
||||
def init_prompts_md():
|
||||
if not os.path.exists(prompts_md_path):
|
||||
with open(prompts_md_path, 'w', encoding='utf-8') as f:
|
||||
f.write('# prompts.chat\n\n')
|
||||
f.write('> A curated list of prompts for ChatGPT and other AI models.\n\n')
|
||||
f.write('---\n\n')
|
||||
|
||||
def append_prompt_to_md(row, prompts_md_path):
|
||||
"""Append a new prompt block to PROMPTS.md"""
|
||||
init_prompts_md(prompts_md_path)
|
||||
block = generate_prompt_block(row)
|
||||
def append_prompt_to_md(row):
|
||||
init_prompts_md()
|
||||
with open(prompts_md_path, 'a', encoding='utf-8') as f:
|
||||
f.write(block)
|
||||
f.write(generate_prompt_block(row))
|
||||
|
||||
def update_prompt_in_md(row, prompts_md_path):
|
||||
"""Update an existing prompt's block in PROMPTS.md"""
|
||||
def update_prompt_in_md(row):
|
||||
act = row.get('act', '')
|
||||
if not os.path.exists(prompts_md_path):
|
||||
append_prompt_to_md(row, prompts_md_path)
|
||||
append_prompt_to_md(row)
|
||||
return
|
||||
|
||||
with open(prompts_md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find and replace the specific prompt block using regex
|
||||
import re
|
||||
# Pattern to match the entire <details> block for this prompt
|
||||
pattern = rf'<details>\n<summary><strong>{re.escape(act)}</strong></summary>.*?</details>\n\n'
|
||||
new_block = generate_prompt_block(row)
|
||||
|
||||
new_content, count = re.subn(pattern, new_block, content, flags=re.DOTALL)
|
||||
|
||||
new_content, count = re.subn(pattern, generate_prompt_block(row), content, flags=re.DOTALL)
|
||||
if count > 0:
|
||||
with open(prompts_md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
else:
|
||||
# Prompt not found, append it
|
||||
append_prompt_to_md(row, prompts_md_path)
|
||||
append_prompt_to_md(row)
|
||||
|
||||
def remove_prompt_from_md(act, prompts_md_path):
|
||||
"""Remove a prompt's block from PROMPTS.md"""
|
||||
def remove_prompt_from_md(act):
|
||||
if not os.path.exists(prompts_md_path):
|
||||
return
|
||||
|
||||
with open(prompts_md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
import re
|
||||
pattern = rf'<details>\n<summary><strong>{re.escape(act)}</strong></summary>.*?</details>\n\n'
|
||||
new_content = re.sub(pattern, '', content, flags=re.DOTALL)
|
||||
|
||||
with open(prompts_md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
prompts_md_path = os.path.join(project_dir, 'PROMPTS.md')
|
||||
# --- Main ---
|
||||
|
||||
local_rows, local_index, fieldnames, skipped_l = read_csv(csv_file)
|
||||
remote_rows, remote_index, remote_fieldnames, skipped_r = read_csv(remote_csv)
|
||||
|
||||
if not fieldnames:
|
||||
fieldnames = remote_fieldnames
|
||||
|
||||
print(f"Local: {len(local_rows)} prompts" + (f" (skipped {skipped_l})" if skipped_l else ""))
|
||||
print(f"Remote: {len(remote_rows)} prompts" + (f" (skipped {skipped_r})" if skipped_r else ""))
|
||||
|
||||
# Compute diffs
|
||||
remote_acts = set(remote_index.keys())
|
||||
new_prompts = []
|
||||
updated_prompts = []
|
||||
deleted_prompts = []
|
||||
|
||||
for row in remote_rows:
|
||||
act = (row.get('act') or '').strip()
|
||||
if not act:
|
||||
continue
|
||||
if act not in local_index:
|
||||
new_prompts.append(row)
|
||||
else:
|
||||
local_row = local_rows[local_index[act]]
|
||||
if (normalize(row.get('prompt')) != normalize(local_row.get('prompt')) or
|
||||
normalize(row.get('contributor')) != normalize(local_row.get('contributor'))):
|
||||
updated_prompts.append(row)
|
||||
|
||||
for act in list(local_index):
|
||||
if act not in remote_acts:
|
||||
deleted_prompts.append(local_rows[local_index[act]])
|
||||
|
||||
print(f"\nNew: {len(new_prompts)}, Updated: {len(updated_prompts)}, Deleted: {len(deleted_prompts)}")
|
||||
|
||||
if not new_prompts and not updated_prompts and not deleted_prompts:
|
||||
print("\nNo CSV changes detected. Already up to date!")
|
||||
else:
|
||||
# Process updates one at a time (apply and commit each update separately)
|
||||
if updated_prompts:
|
||||
print("\nApplying updates to existing prompts...")
|
||||
|
||||
for i, (remote_row, local_row) in enumerate(updated_prompts, 1):
|
||||
act = remote_row.get('act', '').strip()
|
||||
contributor_field = remote_row.get('contributor', '').strip()
|
||||
|
||||
# Update this specific prompt in local_prompts
|
||||
local_prompts[act] = remote_row
|
||||
|
||||
# Rewrite the CSV with this update applied
|
||||
with open(csv_file, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
# Write in order of remote (to maintain order)
|
||||
for row in remote_prompts:
|
||||
row_act = row.get('act', '').strip()
|
||||
if row_act in local_prompts:
|
||||
writer.writerow(local_prompts[row_act])
|
||||
|
||||
# Update only this prompt's block in PROMPTS.md
|
||||
update_prompt_in_md(remote_row, prompts_md_path)
|
||||
|
||||
primary_author, co_authors = parse_contributors(contributor_field)
|
||||
email = f"{primary_author}@users.noreply.github.com"
|
||||
|
||||
subprocess.run(['git', 'add', csv_file, prompts_md_path], check=True)
|
||||
|
||||
# Check if there are actual changes to commit
|
||||
diff_result = subprocess.run(['git', 'diff', '--cached', '--quiet'], capture_output=True)
|
||||
if diff_result.returncode == 0:
|
||||
# No changes staged, skip this commit
|
||||
print(f"[UPDATE {i}/{len(updated_prompts)}] {act} - no changes, skipping")
|
||||
continue
|
||||
|
||||
env = os.environ.copy()
|
||||
env['GIT_AUTHOR_NAME'] = primary_author
|
||||
env['GIT_AUTHOR_EMAIL'] = email
|
||||
env['GIT_COMMITTER_NAME'] = primary_author
|
||||
env['GIT_COMMITTER_EMAIL'] = email
|
||||
|
||||
commit_msg = build_commit_message('Update', act, co_authors)
|
||||
|
||||
subprocess.run([
|
||||
'git', 'commit',
|
||||
'-m', commit_msg,
|
||||
f'--author={primary_author} <{email}>'
|
||||
], env=env, check=True)
|
||||
|
||||
co_authors_str = f" (+ {', '.join(co_authors)})" if co_authors else ""
|
||||
print(f"[UPDATE {i}/{len(updated_prompts)}] {primary_author}{co_authors_str}: {act}")
|
||||
|
||||
# Process new prompts
|
||||
if new_prompts:
|
||||
print("\nCreating commits for new prompts...")
|
||||
|
||||
for i, row in enumerate(new_prompts, 1):
|
||||
contributor_field = row.get('contributor', '').strip()
|
||||
act = row.get('act', 'Unknown')
|
||||
|
||||
primary_author, co_authors = parse_contributors(contributor_field)
|
||||
email = f"{primary_author}@users.noreply.github.com"
|
||||
|
||||
# Append this row to the CSV
|
||||
with open(csv_file, 'a', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writerow(row)
|
||||
|
||||
# Track in local_prompts
|
||||
local_prompts[act] = row
|
||||
|
||||
# Append only this prompt's block to PROMPTS.md
|
||||
append_prompt_to_md(row, prompts_md_path)
|
||||
|
||||
# Stage and commit
|
||||
subprocess.run(['git', 'add', csv_file, prompts_md_path], check=True)
|
||||
|
||||
# Check if there are actual changes to commit
|
||||
diff_result = subprocess.run(['git', 'diff', '--cached', '--quiet'], capture_output=True)
|
||||
if diff_result.returncode == 0:
|
||||
print(f"[NEW {i}/{len(new_prompts)}] {act} - no changes, skipping")
|
||||
continue
|
||||
|
||||
env = os.environ.copy()
|
||||
env['GIT_AUTHOR_NAME'] = primary_author
|
||||
env['GIT_AUTHOR_EMAIL'] = email
|
||||
env['GIT_COMMITTER_NAME'] = primary_author
|
||||
env['GIT_COMMITTER_EMAIL'] = email
|
||||
|
||||
commit_msg = build_commit_message('Add', act, co_authors)
|
||||
|
||||
subprocess.run([
|
||||
'git', 'commit',
|
||||
'-m', commit_msg,
|
||||
f'--author={primary_author} <{email}>'
|
||||
], env=env, check=True)
|
||||
|
||||
co_authors_str = f" (+ {', '.join(co_authors)})" if co_authors else ""
|
||||
print(f"[NEW {i}/{len(new_prompts)}] {primary_author}{co_authors_str}: {act}")
|
||||
|
||||
# Process deleted prompts (remove from CSV, commit with original author)
|
||||
if deleted_prompts:
|
||||
print("\nRemoving unlisted/deleted prompts...")
|
||||
|
||||
for i, row in enumerate(deleted_prompts, 1):
|
||||
contributor_field = row.get('contributor', '').strip()
|
||||
act = row.get('act', 'Unknown')
|
||||
|
||||
primary_author, co_authors = parse_contributors(contributor_field)
|
||||
email = f"{primary_author}@users.noreply.github.com"
|
||||
|
||||
# Remove this prompt from local_prompts
|
||||
if act in local_prompts:
|
||||
del local_prompts[act]
|
||||
|
||||
# Rewrite CSV without the deleted prompt
|
||||
with open(csv_file, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for remaining_act, remaining_row in local_prompts.items():
|
||||
writer.writerow(remaining_row)
|
||||
|
||||
# Remove only this prompt's block from PROMPTS.md
|
||||
remove_prompt_from_md(act, prompts_md_path)
|
||||
|
||||
# Stage and commit
|
||||
subprocess.run(['git', 'add', csv_file, prompts_md_path], check=True)
|
||||
|
||||
# Check if there are actual changes to commit
|
||||
diff_result = subprocess.run(['git', 'diff', '--cached', '--quiet'], capture_output=True)
|
||||
if diff_result.returncode == 0:
|
||||
print(f"[REMOVE {i}/{len(deleted_prompts)}] {act} - no changes, skipping")
|
||||
continue
|
||||
|
||||
env = os.environ.copy()
|
||||
env['GIT_AUTHOR_NAME'] = primary_author
|
||||
env['GIT_AUTHOR_EMAIL'] = email
|
||||
env['GIT_COMMITTER_NAME'] = primary_author
|
||||
env['GIT_COMMITTER_EMAIL'] = email
|
||||
|
||||
commit_msg = build_commit_message('Remove', act, co_authors)
|
||||
|
||||
subprocess.run([
|
||||
'git', 'commit',
|
||||
'-m', commit_msg,
|
||||
f'--author={primary_author} <{email}>'
|
||||
], env=env, check=True)
|
||||
|
||||
co_authors_str = f" (+ {', '.join(co_authors)})" if co_authors else ""
|
||||
print(f"[REMOVE {i}/{len(deleted_prompts)}] {primary_author}{co_authors_str}: {act}")
|
||||
|
||||
print(f"\nDone! Created {len(new_prompts)} new, {len(updated_prompts)} update, {len(deleted_prompts)} remove commits.")
|
||||
print("Already up to date!")
|
||||
sys.exit(0)
|
||||
|
||||
counts = {'add': 0, 'update': 0, 'remove': 0}
|
||||
|
||||
# 1) Deletes — remove one row at a time, keeping other rows untouched
|
||||
if deleted_prompts:
|
||||
print("\nRemoving unlisted/deleted prompts...")
|
||||
for i, row in enumerate(deleted_prompts, 1):
|
||||
act = (row.get('act') or '').strip()
|
||||
contributor = (row.get('contributor') or '').strip()
|
||||
primary, coauthors = parse_contributors(contributor)
|
||||
email = make_email(primary)
|
||||
|
||||
if act in local_index:
|
||||
local_rows.pop(local_index[act])
|
||||
local_index = rebuild_index(local_rows)
|
||||
|
||||
remove_prompt_from_md(act)
|
||||
write_csv(local_rows, fieldnames, csv_file)
|
||||
|
||||
msg = build_commit_msg('Remove', act, coauthors)
|
||||
if git_commit(primary, email, msg):
|
||||
ca = f" (+ {', '.join(coauthors)})" if coauthors else ""
|
||||
print(f" [{i}/{len(deleted_prompts)}] {primary}{ca}: {act}")
|
||||
counts['remove'] += 1
|
||||
else:
|
||||
print(f" [{i}/{len(deleted_prompts)}] {act} — no changes, skipping")
|
||||
|
||||
# 2) Updates — replace only the specific row in-place
|
||||
if updated_prompts:
|
||||
print("\nUpdating existing prompts...")
|
||||
for i, row in enumerate(updated_prompts, 1):
|
||||
act = (row.get('act') or '').strip()
|
||||
contributor = (row.get('contributor') or '').strip()
|
||||
primary, coauthors = parse_contributors(contributor)
|
||||
email = make_email(primary)
|
||||
|
||||
if act in local_index:
|
||||
local_rows[local_index[act]] = row
|
||||
|
||||
update_prompt_in_md(row)
|
||||
write_csv(local_rows, fieldnames, csv_file)
|
||||
|
||||
msg = build_commit_msg('Update', act, coauthors)
|
||||
if git_commit(primary, email, msg):
|
||||
ca = f" (+ {', '.join(coauthors)})" if coauthors else ""
|
||||
print(f" [{i}/{len(updated_prompts)}] {primary}{ca}: {act}")
|
||||
counts['update'] += 1
|
||||
else:
|
||||
print(f" [{i}/{len(updated_prompts)}] {act} — no changes, skipping")
|
||||
|
||||
# 3) New prompts — append one at a time
|
||||
if new_prompts:
|
||||
print("\nAdding new prompts...")
|
||||
for i, row in enumerate(new_prompts, 1):
|
||||
act = (row.get('act') or '').strip()
|
||||
contributor = (row.get('contributor') or '').strip()
|
||||
primary, coauthors = parse_contributors(contributor)
|
||||
email = make_email(primary)
|
||||
|
||||
local_rows.append(row)
|
||||
local_index[act] = len(local_rows) - 1
|
||||
|
||||
append_prompt_to_md(row)
|
||||
write_csv(local_rows, fieldnames, csv_file)
|
||||
|
||||
msg = build_commit_msg('Add', act, coauthors)
|
||||
if git_commit(primary, email, msg):
|
||||
ca = f" (+ {', '.join(coauthors)})" if coauthors else ""
|
||||
print(f" [{i}/{len(new_prompts)}] {primary}{ca}: {act}")
|
||||
counts['add'] += 1
|
||||
else:
|
||||
print(f" [{i}/{len(new_prompts)}] {act} — no changes, skipping")
|
||||
|
||||
print(f"\nDone! {counts['add']} added, {counts['update']} updated, {counts['remove']} removed.")
|
||||
|
||||
PYTHON_SCRIPT
|
||||
PYTHON_EXIT=$?
|
||||
|
||||
@@ -12,7 +12,7 @@ Sentry.init({
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: 1,
|
||||
tracesSampleRate: 0.1,
|
||||
|
||||
// Enable logs to be sent to Sentry
|
||||
enableLogs: true,
|
||||
|
||||
@@ -11,7 +11,7 @@ Sentry.init({
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: 1,
|
||||
tracesSampleRate: 0.1,
|
||||
|
||||
// Enable logs to be sent to Sentry
|
||||
enableLogs: true,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
// Mock all heavy dependencies before importing handler
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
user: { findUnique: vi.fn() },
|
||||
prompt: { findMany: vi.fn(), findFirst: vi.fn(), create: vi.fn() },
|
||||
tag: { findUnique: vi.fn(), create: vi.fn() },
|
||||
$queryRaw: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
mcpGeneralLimiter: { check: vi.fn().mockReturnValue({ allowed: true }) },
|
||||
mcpToolCallLimiter: { check: vi.fn().mockReturnValue({ allowed: true }) },
|
||||
mcpWriteToolLimiter: { check: vi.fn().mockReturnValue({ allowed: true }) },
|
||||
mcpAiToolLimiter: { check: vi.fn().mockReturnValue({ allowed: true }) },
|
||||
}));
|
||||
|
||||
vi.mock("@/../prompts.config", () => ({
|
||||
default: {
|
||||
features: { mcp: true },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api-key", () => ({
|
||||
isValidApiKeyFormat: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/skill-files", () => ({
|
||||
parseSkillFiles: vi.fn(),
|
||||
serializeSkillFiles: vi.fn(),
|
||||
sanitizeFilename: vi.fn(),
|
||||
DEFAULT_SKILL_FILE: "main.md",
|
||||
}));
|
||||
|
||||
function createMockReq(method: string, overrides: Partial<NextApiRequest> = {}): NextApiRequest {
|
||||
return {
|
||||
method,
|
||||
headers: {},
|
||||
url: "/api/mcp",
|
||||
socket: { remoteAddress: "127.0.0.1" },
|
||||
...overrides,
|
||||
} as unknown as NextApiRequest;
|
||||
}
|
||||
|
||||
function createMockRes(): NextApiResponse & {
|
||||
_status: number;
|
||||
_json: unknown;
|
||||
_headers: Record<string, string>;
|
||||
_ended: boolean;
|
||||
} {
|
||||
const res = {
|
||||
_status: 200,
|
||||
_json: null,
|
||||
_headers: {} as Record<string, string>,
|
||||
_ended: false,
|
||||
status(code: number) {
|
||||
res._status = code;
|
||||
return res;
|
||||
},
|
||||
json(data: unknown) {
|
||||
res._json = data;
|
||||
return res;
|
||||
},
|
||||
end() {
|
||||
res._ended = true;
|
||||
return res;
|
||||
},
|
||||
setHeader(key: string, value: string) {
|
||||
res._headers[key] = value;
|
||||
return res;
|
||||
},
|
||||
getHeader(key: string) {
|
||||
return res._headers[key];
|
||||
},
|
||||
headersSent: false,
|
||||
on: vi.fn(),
|
||||
};
|
||||
return res as unknown as NextApiResponse & typeof res;
|
||||
}
|
||||
|
||||
describe("MCP API handler - HTTP method routing", () => {
|
||||
let handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
// Dynamic import to pick up mocks
|
||||
const mod = await import("@/pages/api/mcp");
|
||||
handler = mod.default;
|
||||
});
|
||||
|
||||
describe("GET requests", () => {
|
||||
it("should return 405 Method Not Allowed per MCP Streamable HTTP spec", async () => {
|
||||
const req = createMockReq("GET");
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._status).toBe(405);
|
||||
});
|
||||
|
||||
it("should return JSON-RPC error body", async () => {
|
||||
const req = createMockReq("GET");
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._json).toEqual({
|
||||
jsonrpc: "2.0",
|
||||
error: expect.objectContaining({
|
||||
code: -32000,
|
||||
message: expect.stringContaining("Method not allowed"),
|
||||
}),
|
||||
id: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set Cache-Control: no-store to prevent caching stale 405s", async () => {
|
||||
const req = createMockReq("GET");
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._headers["Cache-Control"]).toBe("no-store");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE requests", () => {
|
||||
it("should return 204 No Content", async () => {
|
||||
const req = createMockReq("DELETE");
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._status).toBe(204);
|
||||
expect(res._ended).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported methods", () => {
|
||||
it("should return 405 for PUT", async () => {
|
||||
const req = createMockReq("PUT");
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._status).toBe(405);
|
||||
});
|
||||
|
||||
it("should return 405 for PATCH", async () => {
|
||||
const req = createMockReq("PATCH");
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._status).toBe(405);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MCP disabled", () => {
|
||||
it("should return 404 when MCP feature is disabled", async () => {
|
||||
// Re-mock config with mcp disabled
|
||||
vi.doMock("@/../prompts.config", () => ({
|
||||
default: { features: { mcp: false } },
|
||||
}));
|
||||
|
||||
// Re-import to get new mock
|
||||
vi.resetModules();
|
||||
const mod = await import("@/pages/api/mcp");
|
||||
const disabledHandler = mod.default;
|
||||
|
||||
const req = createMockReq("GET");
|
||||
const res = createMockRes();
|
||||
|
||||
await disabledHandler(req, res);
|
||||
|
||||
expect(res._status).toBe(404);
|
||||
|
||||
// Restore original mock
|
||||
vi.doMock("@/../prompts.config", () => ({
|
||||
default: { features: { mcp: true } },
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { POST } from "@/app/api/auth/register/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { getConfig } from "@/lib/config";
|
||||
@@ -158,14 +159,15 @@ describe("POST /api/auth/register", () => {
|
||||
});
|
||||
|
||||
describe("duplicate checks", () => {
|
||||
it("should return 400 when email already exists", async () => {
|
||||
// Mock: email check finds existing user
|
||||
vi.mocked(db.user.findUnique).mockImplementation(async (args) => {
|
||||
if (args?.where?.email) {
|
||||
return { id: "1", email: "test@example.com" } as never;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
it("should return 409 when email already exists", async () => {
|
||||
// Mock: create throws P2002 unique violation on email
|
||||
vi.mocked(db.user.create).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
meta: { target: ["email"] },
|
||||
clientVersion: "5.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
const request = createRequest({
|
||||
name: "Test User",
|
||||
@@ -177,14 +179,43 @@ describe("POST /api/auth/register", () => {
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.status).toBe(409);
|
||||
expect(data.error).toBe("email_taken");
|
||||
});
|
||||
|
||||
it("should return 400 when username already exists", async () => {
|
||||
// Mock: email check passes, username check (case-insensitive via findFirst) finds existing user
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.user.findFirst).mockResolvedValue({ id: "1", username: "testuser" } as never);
|
||||
it("should return 409 for case-insensitive email collision", async () => {
|
||||
// Mock: create throws P2002 unique violation on CI email index
|
||||
vi.mocked(db.user.create).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
meta: { target: ["users_email_ci_unique"] },
|
||||
clientVersion: "5.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
const request = createRequest({
|
||||
name: "Test User",
|
||||
username: "testuser",
|
||||
email: "Test@Example.COM",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(data.error).toBe("email_taken");
|
||||
});
|
||||
|
||||
it("should return 409 when username already exists", async () => {
|
||||
// Mock: create throws P2002 unique violation on username
|
||||
vi.mocked(db.user.create).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
meta: { target: ["username"] },
|
||||
clientVersion: "5.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
const request = createRequest({
|
||||
name: "Test User",
|
||||
@@ -196,15 +227,52 @@ describe("POST /api/auth/register", () => {
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(data.error).toBe("username_taken");
|
||||
});
|
||||
|
||||
it("should return 400 for uppercase username", async () => {
|
||||
const request = createRequest({
|
||||
name: "Test User",
|
||||
username: "TestUser",
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("should return 409 for case-insensitive username collision", async () => {
|
||||
// Mock: create throws P2002 unique violation on CI username index
|
||||
vi.mocked(db.user.create).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
meta: { target: ["users_username_ci_unique"] },
|
||||
clientVersion: "5.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
const request = createRequest({
|
||||
name: "Test User",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(data.error).toBe("username_taken");
|
||||
});
|
||||
});
|
||||
|
||||
describe("successful registration", () => {
|
||||
it("should create user and return user data", async () => {
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.user.create).mockResolvedValue({
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
@@ -238,9 +306,44 @@ describe("POST /api/auth/register", () => {
|
||||
expect(data.password).toBeUndefined(); // Password should not be returned
|
||||
});
|
||||
|
||||
it("should lowercase and trim email and username before saving", async () => {
|
||||
vi.mocked(db.user.create).mockResolvedValue({
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
password: "hashed_password",
|
||||
emailVerified: null,
|
||||
image: null,
|
||||
role: "USER",
|
||||
bio: null,
|
||||
credits: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const request = createRequest({
|
||||
name: " Test User ",
|
||||
username: " testuser ",
|
||||
email: " Test@Example.COM ",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
expect(db.user.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
name: "Test User",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should accept valid username with underscores", async () => {
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.user.create).mockResolvedValue({
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { GET, PATCH } from "@/app/api/user/profile/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
@@ -8,6 +9,7 @@ vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
@@ -160,9 +162,15 @@ describe("PATCH /api/user/profile", () => {
|
||||
expect(data.error).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("should return 400 if username is taken", async () => {
|
||||
it("should return 409 if username is taken", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", username: "olduser" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({ id: "other-user" } as never);
|
||||
vi.mocked(db.user.update).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
meta: { target: ["username"] },
|
||||
clientVersion: "5.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/profile", {
|
||||
method: "PATCH",
|
||||
@@ -172,7 +180,29 @@ describe("PATCH /api/user/profile", () => {
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.status).toBe(409);
|
||||
expect(data.error).toBe("username_taken");
|
||||
});
|
||||
|
||||
it("should return 409 for case-insensitive username collision", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", username: "olduser" } } as never);
|
||||
vi.mocked(db.user.update).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
meta: { target: ["users_username_ci_unique"] },
|
||||
clientVersion: "5.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/profile", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: "Test", username: "takenuser" }),
|
||||
});
|
||||
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(data.error).toBe("username_taken");
|
||||
});
|
||||
|
||||
@@ -196,13 +226,12 @@ describe("PATCH /api/user/profile", () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.name).toBe("Updated Name");
|
||||
// Should NOT check for existing username when keeping the same one
|
||||
expect(db.user.findUnique).not.toHaveBeenCalled();
|
||||
// No pre-check needed — uniqueness enforced at DB level
|
||||
expect(db.user.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update profile successfully", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", username: "olduser" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue(null); // Username not taken
|
||||
vi.mocked(db.user.update).mockResolvedValue({
|
||||
id: "user1",
|
||||
name: "New Name",
|
||||
@@ -318,7 +347,7 @@ describe("PATCH /api/user/profile", () => {
|
||||
|
||||
it("should accept valid username with underscores", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", username: "old" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.user.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.user.update).mockResolvedValue({
|
||||
id: "user1",
|
||||
name: "Test",
|
||||
|
||||
@@ -29,6 +29,10 @@ describe("GET /api/prompts/[id]/versions", () => {
|
||||
});
|
||||
|
||||
it("should return empty array for prompt with no versions", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([]);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions");
|
||||
@@ -42,6 +46,10 @@ describe("GET /api/prompts/[id]/versions", () => {
|
||||
});
|
||||
|
||||
it("should return versions ordered by version desc", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "v3",
|
||||
@@ -83,6 +91,10 @@ describe("GET /api/prompts/[id]/versions", () => {
|
||||
});
|
||||
|
||||
it("should include author info in response", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "v1",
|
||||
@@ -106,6 +118,10 @@ describe("GET /api/prompts/[id]/versions", () => {
|
||||
});
|
||||
|
||||
it("should call findMany with correct parameters", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([]);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions");
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { getProviderMetadata, getConfiguredProviderIds } from "@/lib/auth/provider-metadata";
|
||||
|
||||
type ConfigParam = Parameters<typeof getConfiguredProviderIds>[0];
|
||||
|
||||
describe("Provider Metadata", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe("getProviderMetadata", () => {
|
||||
it("should return empty maps when no generic providers are requested", () => {
|
||||
const result = getProviderMetadata(["github", "google"]);
|
||||
expect(result.displayNames).toEqual({});
|
||||
expect(result.logos).toEqual({});
|
||||
});
|
||||
|
||||
it("should read OIDC display name and logo from env", () => {
|
||||
process.env.AUTH_OIDC_NAME = "Corporate SSO";
|
||||
process.env.AUTH_OIDC_LOGO = "https://example.com/logo.svg";
|
||||
|
||||
const result = getProviderMetadata(["github", "oidc"]);
|
||||
expect(result.displayNames).toEqual({ oidc: "Corporate SSO" });
|
||||
expect(result.logos).toEqual({ oidc: "https://example.com/logo.svg" });
|
||||
});
|
||||
|
||||
it("should read OAuth display name and logo from env", () => {
|
||||
process.env.AUTH_OAUTH_NAME = "Legacy IdP";
|
||||
process.env.AUTH_OAUTH_LOGO = "https://idp.example.com/icon.png";
|
||||
|
||||
const result = getProviderMetadata(["oauth"]);
|
||||
expect(result.displayNames).toEqual({ oauth: "Legacy IdP" });
|
||||
expect(result.logos).toEqual({ oauth: "https://idp.example.com/icon.png" });
|
||||
});
|
||||
|
||||
it("should return both OIDC and OAuth metadata when both are configured", () => {
|
||||
process.env.AUTH_OIDC_NAME = "OIDC Provider";
|
||||
process.env.AUTH_OIDC_LOGO = "https://oidc.example.com/logo.svg";
|
||||
process.env.AUTH_OAUTH_NAME = "OAuth Provider";
|
||||
process.env.AUTH_OAUTH_LOGO = "https://oauth.example.com/logo.svg";
|
||||
|
||||
const result = getProviderMetadata(["oidc", "oauth"]);
|
||||
expect(result.displayNames).toEqual({
|
||||
oidc: "OIDC Provider",
|
||||
oauth: "OAuth Provider",
|
||||
});
|
||||
expect(result.logos).toEqual({
|
||||
oidc: "https://oidc.example.com/logo.svg",
|
||||
oauth: "https://oauth.example.com/logo.svg",
|
||||
});
|
||||
});
|
||||
|
||||
it("should omit name when env var is not set", () => {
|
||||
process.env.AUTH_OIDC_LOGO = "https://example.com/logo.svg";
|
||||
// AUTH_OIDC_NAME not set
|
||||
|
||||
const result = getProviderMetadata(["oidc"]);
|
||||
expect(result.displayNames).toEqual({});
|
||||
expect(result.logos).toEqual({ oidc: "https://example.com/logo.svg" });
|
||||
});
|
||||
|
||||
it("should omit logo when env var is not set", () => {
|
||||
process.env.AUTH_OIDC_NAME = "My Provider";
|
||||
// AUTH_OIDC_LOGO not set
|
||||
|
||||
const result = getProviderMetadata(["oidc"]);
|
||||
expect(result.displayNames).toEqual({ oidc: "My Provider" });
|
||||
expect(result.logos).toEqual({});
|
||||
});
|
||||
|
||||
it("should reject logo URLs with javascript: protocol", () => {
|
||||
process.env.AUTH_OIDC_NAME = "Evil Provider";
|
||||
process.env.AUTH_OIDC_LOGO = "javascript:alert('xss')";
|
||||
|
||||
const result = getProviderMetadata(["oidc"]);
|
||||
expect(result.displayNames).toEqual({ oidc: "Evil Provider" });
|
||||
expect(result.logos).toEqual({});
|
||||
});
|
||||
|
||||
it("should reject logo URLs with data: protocol", () => {
|
||||
process.env.AUTH_OIDC_LOGO = "data:image/svg+xml,<svg></svg>";
|
||||
|
||||
const result = getProviderMetadata(["oidc"]);
|
||||
expect(result.logos).toEqual({});
|
||||
});
|
||||
|
||||
it("should accept http:// logo URLs", () => {
|
||||
process.env.AUTH_OIDC_LOGO = "http://internal.corp.com/logo.png";
|
||||
|
||||
const result = getProviderMetadata(["oidc"]);
|
||||
expect(result.logos).toEqual({ oidc: "http://internal.corp.com/logo.png" });
|
||||
});
|
||||
|
||||
it("should reject malformed URLs", () => {
|
||||
process.env.AUTH_OIDC_LOGO = "not-a-url";
|
||||
|
||||
const result = getProviderMetadata(["oidc"]);
|
||||
expect(result.logos).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getConfiguredProviderIds", () => {
|
||||
it("should return providers array when configured", () => {
|
||||
const config = {
|
||||
auth: { providers: ["github", "google", "oidc"], allowRegistration: false },
|
||||
} as unknown as ConfigParam;
|
||||
|
||||
expect(getConfiguredProviderIds(config)).toEqual(["github", "google", "oidc"]);
|
||||
});
|
||||
|
||||
it("should fallback to deprecated singular provider string if present", () => {
|
||||
const config = {
|
||||
auth: { provider: "github", allowRegistration: false },
|
||||
} as unknown as ConfigParam;
|
||||
|
||||
expect(getConfiguredProviderIds(config)).toEqual(["github"]);
|
||||
});
|
||||
|
||||
it("should default to credentials when providers array is empty", () => {
|
||||
const config = {
|
||||
auth: { providers: [], allowRegistration: false },
|
||||
} as unknown as ConfigParam;
|
||||
|
||||
expect(getConfiguredProviderIds(config)).toEqual(["credentials"]);
|
||||
});
|
||||
|
||||
it("should default to credentials when providers is not set", () => {
|
||||
const config = {
|
||||
auth: { allowRegistration: false },
|
||||
} as unknown as ConfigParam;
|
||||
|
||||
expect(getConfiguredProviderIds(config)).toEqual(["credentials"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- tests inspect the dynamic NextAuth provider object */
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { oauthPlugin } from "@/lib/plugins/auth/oauth";
|
||||
|
||||
describe("OAuth 2.0 Auth Plugin", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
process.env.AUTH_OAUTH_ID = "test-client-id";
|
||||
process.env.AUTH_OAUTH_SECRET = "test-client-secret";
|
||||
process.env.AUTH_OAUTH_ISSUER = "https://sso.test.com";
|
||||
process.env.AUTH_OAUTH_NAME = "Test OAuth";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it("should have correct plugin id and name", () => {
|
||||
expect(oauthPlugin.id).toBe("oauth");
|
||||
expect(oauthPlugin.name).toBe("Generic OAuth 2.0");
|
||||
});
|
||||
|
||||
it("should have PKCE enabled by default", () => {
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
expect(provider.checks).toBeUndefined(); // Enabled by default
|
||||
});
|
||||
|
||||
it("should allow disabling PKCE explicitly", () => {
|
||||
process.env.AUTH_OAUTH_ENABLE_PKCE = "false";
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
expect(provider.checks).toEqual(["state"]);
|
||||
});
|
||||
|
||||
it("should configure standard OAuth provider dynamically from env", () => {
|
||||
process.env.AUTH_OAUTH_ID = "test-client-id";
|
||||
process.env.AUTH_OAUTH_SECRET = "test-client-secret";
|
||||
process.env.AUTH_OAUTH_ISSUER = "https://sso.test.com";
|
||||
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
|
||||
expect(provider.id).toBe("oauth");
|
||||
expect(provider.type).toBe("oauth");
|
||||
expect(provider.clientId).toBe("test-client-id");
|
||||
expect(provider.clientSecret).toBe("test-client-secret");
|
||||
expect(provider.issuer).toBe("https://sso.test.com");
|
||||
|
||||
// Check fallback URLs derived from issuer
|
||||
expect(provider.authorization.url).toBe("https://sso.test.com/authorize");
|
||||
expect(provider.token).toBe("https://sso.test.com/token");
|
||||
expect(provider.userinfo).toBe("https://sso.test.com/userinfo");
|
||||
|
||||
// Check default scope (should NOT include openid for relaxed OAuth)
|
||||
expect(provider.authorization.params.scope).toBe("email profile");
|
||||
});
|
||||
|
||||
it("should use explicit URL overrides when provided", () => {
|
||||
process.env.AUTH_OAUTH_ID = "test-client-id";
|
||||
process.env.AUTH_OAUTH_SECRET = "test-client-secret";
|
||||
process.env.AUTH_OAUTH_ISSUER = "https://sso.test.com";
|
||||
|
||||
process.env.AUTH_OAUTH_AUTHORIZATION_URL = "https://custom.test.com/auth";
|
||||
process.env.AUTH_OAUTH_TOKEN_URL = "https://custom.test.com/token";
|
||||
process.env.AUTH_OAUTH_USERINFO_URL = "https://custom.test.com/me";
|
||||
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
|
||||
expect(provider.authorization.url).toBe("https://custom.test.com/auth");
|
||||
expect(provider.token).toBe("https://custom.test.com/token");
|
||||
expect(provider.userinfo).toBe("https://custom.test.com/me");
|
||||
});
|
||||
|
||||
it("should handle wellknown discovery correctly", () => {
|
||||
process.env.AUTH_OAUTH_ID = "test-client-id";
|
||||
process.env.AUTH_OAUTH_SECRET = "test-client-secret";
|
||||
process.env.AUTH_OAUTH_ISSUER = "https://sso.test.com";
|
||||
process.env.AUTH_OAUTH_WELLKNOWN = "https://sso.test.com/.well-known";
|
||||
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
|
||||
expect(provider.wellKnown).toBe("https://sso.test.com/.well-known");
|
||||
expect(provider.authorization.url).toBeUndefined(); // Let wellKnown handle it
|
||||
expect(provider.token).toBeUndefined();
|
||||
expect(provider.userinfo).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle custom style logo", () => {
|
||||
process.env.AUTH_OAUTH_LOGO = "https://logo.com/image.png";
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
|
||||
expect(provider.style?.logo).toBe("https://logo.com/image.png");
|
||||
});
|
||||
|
||||
it("should process user profile mappings correctly", () => {
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
|
||||
const mockProfile = {
|
||||
sub: "12345",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
picture: "https://avatar.com/me.png",
|
||||
preferred_username: "testuser"
|
||||
};
|
||||
|
||||
const parsedProfile = provider.profile(mockProfile);
|
||||
|
||||
expect(parsedProfile.id).toBe("12345");
|
||||
expect(parsedProfile.name).toBe("Test User");
|
||||
expect(parsedProfile.email).toBe("test@example.com");
|
||||
expect(parsedProfile.image).toBe("https://avatar.com/me.png");
|
||||
expect(parsedProfile.username).toBe("testuser");
|
||||
});
|
||||
|
||||
it("should fallback correctly if standard profile fields are missing", () => {
|
||||
const provider: any = oauthPlugin.getProvider();
|
||||
|
||||
const weirdProfile = {
|
||||
id: 999, // Testing numeric ID coercion
|
||||
email: "weird@example.com",
|
||||
avatar_url: "https://avatar.com/me2.png"
|
||||
};
|
||||
|
||||
const parsedProfile = provider.profile(weirdProfile);
|
||||
|
||||
expect(parsedProfile.id).toBe("999");
|
||||
expect(parsedProfile.name).toBe("weird@example.com"); // Fallback to email
|
||||
expect(parsedProfile.email).toBe("weird@example.com");
|
||||
expect(parsedProfile.image).toBe("https://avatar.com/me2.png");
|
||||
expect(parsedProfile.username).toBe("weird"); // Fallback to email prefix
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- tests inspect the dynamic NextAuth provider object */
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { oidcPlugin } from "@/lib/plugins/auth/oidc";
|
||||
|
||||
describe("OpenID Connect Auth Plugin", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
process.env.AUTH_OIDC_ID = "test-client-id";
|
||||
process.env.AUTH_OIDC_SECRET = "test-client-secret";
|
||||
process.env.AUTH_OIDC_ISSUER = "https://sso.test.com";
|
||||
process.env.AUTH_OIDC_NAME = "Test OIDC";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it("should have correct plugin id and name", () => {
|
||||
expect(oidcPlugin.id).toBe("oidc");
|
||||
expect(oidcPlugin.name).toBe("Generic OIDC");
|
||||
});
|
||||
|
||||
it("should configure standard OIDC provider dynamically from env", () => {
|
||||
process.env.AUTH_OIDC_ID = "test-client-id";
|
||||
process.env.AUTH_OIDC_SECRET = "test-client-secret";
|
||||
process.env.AUTH_OIDC_ISSUER = "https://sso.test.com";
|
||||
|
||||
const provider: any = oidcPlugin.getProvider();
|
||||
|
||||
expect(provider.id).toBe("oidc");
|
||||
expect(provider.type).toBe("oidc");
|
||||
expect(provider.clientId).toBe("test-client-id");
|
||||
expect(provider.clientSecret).toBe("test-client-secret");
|
||||
expect(provider.issuer).toBe("https://sso.test.com");
|
||||
expect(provider.wellKnown).toBe("https://sso.test.com/.well-known/openid-configuration");
|
||||
});
|
||||
|
||||
it("should support well-known discovery override", () => {
|
||||
process.env.AUTH_OIDC_ISSUER = "https://sso.test.com";
|
||||
process.env.AUTH_OIDC_WELLKNOWN = "https://sso.test.com/custom-well-known";
|
||||
|
||||
const provider: any = oidcPlugin.getProvider();
|
||||
expect(provider.wellKnown).toBe("https://sso.test.com/custom-well-known");
|
||||
});
|
||||
|
||||
it("should have PKCE enabled by default", () => {
|
||||
delete process.env.AUTH_OIDC_ENABLE_PKCE;
|
||||
const provider: any = oidcPlugin.getProvider();
|
||||
expect(provider.checks).toBeUndefined(); // Enabled by default
|
||||
});
|
||||
|
||||
it("should apply PKCE bypass if explicitly disabled", () => {
|
||||
process.env.AUTH_OIDC_ENABLE_PKCE = "false";
|
||||
const provider: any = oidcPlugin.getProvider();
|
||||
expect(provider.checks).toEqual(["state"]);
|
||||
});
|
||||
|
||||
it("should keep PKCE enabled if explicitly turned on", () => {
|
||||
process.env.AUTH_OIDC_ENABLE_PKCE = "true";
|
||||
const provider: any = oidcPlugin.getProvider();
|
||||
expect(provider.checks).toBeUndefined(); // Auth.js will handle PKCE natively
|
||||
});
|
||||
|
||||
it("should handle custom style logo", () => {
|
||||
process.env.AUTH_OIDC_LOGO = "https://logo.com/image.png";
|
||||
const provider: any = oidcPlugin.getProvider();
|
||||
|
||||
expect(provider.style?.logo).toBe("https://logo.com/image.png");
|
||||
});
|
||||
|
||||
it("should fall back to standard UserInfo fields correctly", () => {
|
||||
process.env.AUTH_OIDC_ID = "test-client-id";
|
||||
process.env.AUTH_OIDC_SECRET = "test-client-secret";
|
||||
process.env.AUTH_OIDC_ISSUER = "https://sso.test.com";
|
||||
|
||||
const provider: any = oidcPlugin.getProvider();
|
||||
|
||||
const mockProfile = {
|
||||
sub: "12345",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
picture: "https://avatar.com/me.png",
|
||||
preferred_username: "testuser"
|
||||
};
|
||||
|
||||
const parsedProfile: any = provider.profile(mockProfile);
|
||||
|
||||
expect(parsedProfile.id).toBe("12345");
|
||||
expect(parsedProfile.name).toBe("Test User");
|
||||
expect(parsedProfile.email).toBe("test@example.com");
|
||||
expect(parsedProfile.image).toBe("https://avatar.com/me.png");
|
||||
expect(parsedProfile.username).toBe("testuser");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { canViewPrompt, checkPromptAccess } from "@/lib/prompt-access";
|
||||
import { auth } from "@/lib/auth";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("canViewPrompt", () => {
|
||||
it("should return false for null prompt", () => {
|
||||
expect(canViewPrompt(null, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for public prompt with no session", () => {
|
||||
expect(
|
||||
canViewPrompt({ isPrivate: false, authorId: "user1" }, null)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for public prompt with any session", () => {
|
||||
const session = { user: { id: "other" } } as Session;
|
||||
expect(
|
||||
canViewPrompt({ isPrivate: false, authorId: "user1" }, session)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for private prompt with no session", () => {
|
||||
expect(
|
||||
canViewPrompt({ isPrivate: true, authorId: "user1" }, null)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for private prompt when user is not owner", () => {
|
||||
const session = { user: { id: "other", role: "USER" } } as Session;
|
||||
expect(
|
||||
canViewPrompt({ isPrivate: true, authorId: "user1" }, session)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for private prompt when user is the owner", () => {
|
||||
const session = { user: { id: "user1", role: "USER" } } as Session;
|
||||
expect(
|
||||
canViewPrompt({ isPrivate: true, authorId: "user1" }, session)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for private prompt when user is admin", () => {
|
||||
const session = { user: { id: "admin1", role: "ADMIN" } } as Session;
|
||||
expect(
|
||||
canViewPrompt({ isPrivate: true, authorId: "user1" }, session)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for private prompt with session missing user", () => {
|
||||
const session = {} as Session;
|
||||
expect(
|
||||
canViewPrompt({ isPrivate: true, authorId: "user1" }, session)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkPromptAccess", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 404 for null prompt", async () => {
|
||||
const result = await checkPromptAccess(null);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.status).toBe(404);
|
||||
const data = await result!.json();
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should return null for public prompt (no auth call)", async () => {
|
||||
const result = await checkPromptAccess({ isPrivate: false, authorId: "user1" });
|
||||
expect(result).toBeNull();
|
||||
expect(auth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return 404 for private prompt with no session", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
const result = await checkPromptAccess({ isPrivate: true, authorId: "user1" });
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.status).toBe(404);
|
||||
expect(auth).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should return 404 for private prompt when user is not owner", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "other", role: "USER" } } as never);
|
||||
const result = await checkPromptAccess({ isPrivate: true, authorId: "user1" });
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should return null for private prompt when user is the owner", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
const result = await checkPromptAccess({ isPrivate: true, authorId: "user1" });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for private prompt when user is admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
const result = await checkPromptAccess({ isPrivate: true, authorId: "user1" });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getConfig } from "@/lib/config";
|
||||
import { getConfiguredProviderIds, getProviderMetadata } from "@/lib/auth/provider-metadata";
|
||||
import { AuthContent } from "@/components/auth/auth-content";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -9,24 +10,14 @@ export const metadata: Metadata = {
|
||||
description: "Login to your account",
|
||||
};
|
||||
|
||||
// Helper to get providers from config (supports both old `provider` and new `providers` array)
|
||||
function getProviders(config: Awaited<ReturnType<typeof getConfig>>): string[] {
|
||||
if (config.auth.providers && config.auth.providers.length > 0) {
|
||||
return config.auth.providers;
|
||||
}
|
||||
if (config.auth.provider) {
|
||||
return [config.auth.provider];
|
||||
}
|
||||
return ["credentials"];
|
||||
}
|
||||
|
||||
export default async function LoginPage() {
|
||||
const t = await getTranslations("auth");
|
||||
const config = await getConfig();
|
||||
const providers = getProviders(config);
|
||||
const providers = getConfiguredProviderIds(config);
|
||||
const hasCredentials = providers.includes("credentials");
|
||||
const hasOnlyCredentials = providers.length === 1 && hasCredentials;
|
||||
const useCloneBranding = config.homepage?.useCloneBranding ?? false;
|
||||
const { displayNames, logos } = getProviderMetadata(providers);
|
||||
|
||||
return (
|
||||
<div className="container flex min-h-[calc(100vh-6rem)] flex-col items-center justify-center py-8">
|
||||
@@ -38,7 +29,7 @@ export default async function LoginPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<AuthContent providers={providers} mode="login" useCloneBranding={useCloneBranding} />
|
||||
<AuthContent providers={providers} mode="login" useCloneBranding={useCloneBranding} providerDisplayNames={displayNames} providerLogos={logos} />
|
||||
</div>
|
||||
{hasCredentials && (
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getConfig } from "@/lib/config";
|
||||
import { getConfiguredProviderIds, getProviderMetadata } from "@/lib/auth/provider-metadata";
|
||||
import { AuthContent } from "@/components/auth/auth-content";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -10,21 +11,10 @@ export const metadata: Metadata = {
|
||||
description: "Create a new account",
|
||||
};
|
||||
|
||||
// Helper to get providers from config (supports both old `provider` and new `providers` array)
|
||||
function getProviders(config: Awaited<ReturnType<typeof getConfig>>): string[] {
|
||||
if (config.auth.providers && config.auth.providers.length > 0) {
|
||||
return config.auth.providers;
|
||||
}
|
||||
if (config.auth.provider) {
|
||||
return [config.auth.provider];
|
||||
}
|
||||
return ["credentials"];
|
||||
}
|
||||
|
||||
export default async function RegisterPage() {
|
||||
const t = await getTranslations("auth");
|
||||
const config = await getConfig();
|
||||
const providers = getProviders(config);
|
||||
const providers = getConfiguredProviderIds(config);
|
||||
const hasCredentials = providers.includes("credentials");
|
||||
|
||||
// Block registration if disabled or credentials not enabled
|
||||
@@ -32,6 +22,8 @@ export default async function RegisterPage() {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const { displayNames, logos } = getProviderMetadata(providers);
|
||||
|
||||
return (
|
||||
<div className="container flex min-h-[calc(100vh-6rem)] flex-col items-center justify-center py-8">
|
||||
<div className="w-full max-w-sm space-y-4">
|
||||
@@ -40,7 +32,7 @@ export default async function RegisterPage() {
|
||||
<p className="text-xs text-muted-foreground">{t("registerDescription")}</p>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<AuthContent providers={providers} mode="register" />
|
||||
<AuthContent providers={providers} mode="register" providerDisplayNames={displayNames} providerLogos={logos} />
|
||||
</div>
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
{t("hasAccount")}{" "}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const size = {
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
export const revalidate = 3600;
|
||||
|
||||
const radiusMap: Record<string, number> = {
|
||||
none: 0,
|
||||
@@ -49,6 +50,7 @@ export default async function OGImage({ params }: { params: Promise<{ username:
|
||||
|
||||
const user = await db.user.findFirst({
|
||||
where: { username: { equals: username, mode: "insensitive" } },
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
|
||||
@@ -38,6 +38,7 @@ export async function generateMetadata({ params }: UserProfilePageProps): Promis
|
||||
|
||||
const user = await db.user.findFirst({
|
||||
where: { username: { equals: username, mode: "insensitive" } },
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: { name: true, username: true },
|
||||
});
|
||||
|
||||
@@ -72,6 +73,7 @@ export default async function UserProfilePage({ params, searchParams }: UserProf
|
||||
|
||||
const user = await db.user.findFirst({
|
||||
where: { username: { equals: username, mode: "insensitive" } },
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
|
||||
@@ -124,6 +124,11 @@ const coreContributors = [
|
||||
role: "Chrome Extension Maintainer",
|
||||
x: "fatihsolhann",
|
||||
},
|
||||
{
|
||||
username: "JohnPhamous",
|
||||
role: "Performance (Vercel)",
|
||||
x: "JohnPhamous",
|
||||
},
|
||||
{
|
||||
username: "claude-opus-4.5",
|
||||
displayName: "Claude Opus 4.5",
|
||||
@@ -169,6 +174,15 @@ const designCredits = [
|
||||
},
|
||||
];
|
||||
|
||||
const securityCredits = [
|
||||
{
|
||||
username: "mdisec",
|
||||
displayName: "Mehmet Ince",
|
||||
role: "Security Fixes",
|
||||
x: "mdisec",
|
||||
},
|
||||
];
|
||||
|
||||
const ideationCredits = [
|
||||
{
|
||||
username: "semihkislar",
|
||||
@@ -566,6 +580,53 @@ export default async function AboutPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Security */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-lg font-semibold mb-4">{t("securityTitle")}</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{securityCredits.map((contributor) => (
|
||||
<div
|
||||
key={contributor.username}
|
||||
className="flex items-center gap-3 p-3 border rounded-lg"
|
||||
>
|
||||
<Image
|
||||
src={`https://github.com/${contributor.username}.png`}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm">
|
||||
{contributor.displayName ?? `@${contributor.username}`}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{contributor.role}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`https://github.com/${contributor.username}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1.5 rounded-md hover:bg-muted transition-colors"
|
||||
title="GitHub"
|
||||
>
|
||||
<BrandIcon name="github" />
|
||||
</Link>
|
||||
<Link
|
||||
href={`https://x.com/${contributor.x}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1.5 rounded-md hover:bg-muted transition-colors"
|
||||
title="X"
|
||||
>
|
||||
<BrandIcon name="x" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Ideation */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-lg font-semibold mb-4">{t("ideationTitle")}</h2>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const domain = process.env.EZOIC_SITE_DOMAIN;
|
||||
|
||||
if (!domain) {
|
||||
return new NextResponse("# ads.txt not configured\n", {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.redirect(
|
||||
`https://srv.adstxtmanager.com/19390/${domain}`,
|
||||
301
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,16 @@ import { NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { isUniqueConstraintViolation } from "@/lib/db-errors";
|
||||
import { getConfig } from "@/lib/config";
|
||||
|
||||
// Trim before validation to prevent unicode/whitespace tricks that bypass uniqueness checks (e.g. "admin@x.com\u200B" vs "admin@x.com") on certain DBMS
|
||||
const trimmed = z.preprocess((v) => (typeof v === "string" ? v.trim() : v), z.string());
|
||||
|
||||
const registerSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
username: z.string().min(1).regex(/^[a-zA-Z0-9_]+$/),
|
||||
email: z.string().email(),
|
||||
name: trimmed.pipe(z.string().min(2)),
|
||||
username: trimmed.pipe(z.string().min(1).max(30).regex(/^[a-z0-9_]+$/)),
|
||||
email: trimmed.pipe(z.string().email()).transform((v) => v.toLowerCase()),
|
||||
password: z.string().min(6),
|
||||
});
|
||||
|
||||
@@ -34,49 +38,41 @@ export async function POST(request: Request) {
|
||||
|
||||
const { name, username, email, password } = parsed.data;
|
||||
|
||||
// Check if email already exists
|
||||
const existingEmail = await db.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (existingEmail) {
|
||||
return NextResponse.json(
|
||||
{ error: "email_taken", message: "Email is already taken" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if username already exists (case-insensitive)
|
||||
const existingUsername = await db.user.findFirst({
|
||||
where: { username: { equals: username, mode: "insensitive" } },
|
||||
});
|
||||
|
||||
if (existingUsername) {
|
||||
return NextResponse.json(
|
||||
{ error: "username_taken", message: "Username is already taken" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// Create user
|
||||
const user = await db.user.create({
|
||||
data: {
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
// Atomic create — DB unique constraints enforce email and CI username uniqueness
|
||||
try {
|
||||
const user = await db.user.create({
|
||||
data: {
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
});
|
||||
return NextResponse.json({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUniqueConstraintViolation(error, "email")) {
|
||||
return NextResponse.json(
|
||||
{ error: "email_taken", message: "Email is already taken" },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
if (isUniqueConstraintViolation(error, "username")) {
|
||||
return NextResponse.json(
|
||||
{ error: "username_taken", message: "Username is already taken" },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { checkPromptAccess } from "@/lib/prompt-access";
|
||||
|
||||
const updateChangeRequestSchema = z.object({
|
||||
status: z.enum(["APPROVED", "REJECTED", "PENDING"]),
|
||||
@@ -196,7 +197,8 @@ export async function GET(
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
content: true,
|
||||
isPrivate: true,
|
||||
authorId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -209,6 +211,9 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
|
||||
const denied = await checkPromptAccess(changeRequest.prompt);
|
||||
if (denied) return denied;
|
||||
|
||||
return NextResponse.json(changeRequest);
|
||||
} catch (error) {
|
||||
console.error("Get change request error:", error);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { checkPromptAccess } from "@/lib/prompt-access";
|
||||
|
||||
const createChangeRequestSchema = z.object({
|
||||
proposedContent: z.string().min(1),
|
||||
@@ -94,6 +95,14 @@ export async function GET(
|
||||
try {
|
||||
const { id: promptId } = await params;
|
||||
|
||||
const prompt = await db.prompt.findUnique({
|
||||
where: { id: promptId },
|
||||
select: { isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
const denied = await checkPromptAccess(prompt);
|
||||
if (denied) return denied;
|
||||
|
||||
const changeRequests = await db.changeRequest.findMany({
|
||||
where: { promptId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { z } from "zod";
|
||||
import { checkPromptAccess } from "@/lib/prompt-access";
|
||||
|
||||
const addExampleSchema = z.object({
|
||||
mediaUrl: z.string().url(),
|
||||
@@ -16,12 +17,11 @@ export async function GET(
|
||||
|
||||
const prompt = await db.prompt.findUnique({
|
||||
where: { id: promptId },
|
||||
select: { id: true, type: true },
|
||||
select: { id: true, type: true, isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
if (!prompt) {
|
||||
return NextResponse.json({ error: "Prompt not found" }, { status: 404 });
|
||||
}
|
||||
const denied = await checkPromptAccess(prompt);
|
||||
if (denied || !prompt) return denied!;
|
||||
|
||||
// Only allow examples for IMAGE and VIDEO prompts
|
||||
if (prompt.type !== "IMAGE" && prompt.type !== "VIDEO") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { parseSkillFiles } from "@/lib/skill-files";
|
||||
import { parseSkillFiles, sanitizeFilename } from "@/lib/skill-files";
|
||||
import JSZip from "jszip";
|
||||
|
||||
/**
|
||||
@@ -53,9 +53,18 @@ export async function GET(
|
||||
// Create a zip file
|
||||
const zip = new JSZip();
|
||||
|
||||
// Add each file to the zip
|
||||
// Add each file to the zip (sanitize filenames to prevent Zip Slip)
|
||||
for (const file of files) {
|
||||
zip.file(file.filename, file.content);
|
||||
const candidate = file.filename
|
||||
.replace(/\\/g, "/")
|
||||
.split("/")
|
||||
.filter((segment) => segment && segment !== "." && segment !== "..")
|
||||
.join("/");
|
||||
const safeName = sanitizeFilename(candidate);
|
||||
if (!safeName) {
|
||||
continue;
|
||||
}
|
||||
zip.file(safeName, file.content);
|
||||
}
|
||||
|
||||
// Generate the zip content as blob for Response compatibility
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { checkPromptAccess } from "@/lib/prompt-access";
|
||||
|
||||
const createVersionSchema = z.object({
|
||||
content: z.string().min(1, "Content is required"),
|
||||
@@ -116,6 +117,14 @@ export async function GET(
|
||||
try {
|
||||
const { id: promptId } = await params;
|
||||
|
||||
const prompt = await db.prompt.findUnique({
|
||||
where: { id: promptId },
|
||||
select: { isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
const denied = await checkPromptAccess(prompt);
|
||||
if (denied) return denied;
|
||||
|
||||
const versions = await db.promptVersion.findMany({
|
||||
where: { promptId },
|
||||
orderBy: { version: "desc" },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { checkPromptAccess } from "@/lib/prompt-access";
|
||||
|
||||
// POST - Upvote a prompt
|
||||
export async function POST(
|
||||
@@ -21,14 +22,11 @@ export async function POST(
|
||||
// Check if prompt exists
|
||||
const prompt = await db.prompt.findUnique({
|
||||
where: { id: promptId },
|
||||
select: { isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
if (!prompt) {
|
||||
return NextResponse.json(
|
||||
{ error: "not_found", message: "Prompt not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
const denied = await checkPromptAccess(prompt);
|
||||
if (denied) return denied;
|
||||
|
||||
// Check if already voted
|
||||
const existing = await db.promptVote.findUnique({
|
||||
@@ -86,6 +84,14 @@ export async function DELETE(
|
||||
|
||||
const { id: promptId } = await params;
|
||||
|
||||
const prompt = await db.prompt.findUnique({
|
||||
where: { id: promptId },
|
||||
select: { isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
const denied = await checkPromptAccess(prompt);
|
||||
if (denied) return denied;
|
||||
|
||||
// Delete vote
|
||||
await db.promptVote.deleteMany({
|
||||
where: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { isUniqueConstraintViolation } from "@/lib/db-errors";
|
||||
|
||||
const customLinkSchema = z.object({
|
||||
type: z.enum(["website", "github", "twitter", "linkedin", "instagram", "youtube", "twitch", "discord", "mastodon", "bluesky", "sponsor"]),
|
||||
@@ -10,13 +11,12 @@ const customLinkSchema = z.object({
|
||||
label: z.string().max(30).optional(),
|
||||
});
|
||||
|
||||
// Trim before validation to prevent unicode/whitespace tricks that bypass uniqueness checks (e.g. "admin\u200B@x.com" vs "admin@x.com")
|
||||
const trimmed = z.preprocess((v) => (typeof v === "string" ? v.trim() : v), z.string());
|
||||
|
||||
const updateProfileSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
username: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(30)
|
||||
.regex(/^[a-zA-Z0-9_]+$/),
|
||||
name: trimmed.pipe(z.string().min(1).max(100)),
|
||||
username: trimmed.pipe(z.string().min(1).max(30).regex(/^[a-z0-9_]+$/)),
|
||||
avatar: z.string().url().optional().or(z.literal("")),
|
||||
bio: z.string().max(250).optional().or(z.literal("")),
|
||||
customLinks: z.array(customLinkSchema).max(5).optional(),
|
||||
@@ -44,43 +44,38 @@ export async function PATCH(request: NextRequest) {
|
||||
|
||||
const { name, username, avatar, bio, customLinks } = parsed.data;
|
||||
|
||||
// Check if username is taken by another user
|
||||
if (username !== session.user.username) {
|
||||
const existingUser = await db.user.findUnique({
|
||||
where: { username },
|
||||
select: { id: true },
|
||||
// Atomic update — DB-level CI unique index prevents collisions
|
||||
try {
|
||||
const user = await db.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: {
|
||||
name,
|
||||
username,
|
||||
avatar: avatar || null,
|
||||
bio: bio || null,
|
||||
customLinks: customLinks && customLinks.length > 0 ? customLinks : Prisma.DbNull,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
bio: true,
|
||||
customLinks: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser && existingUser.id !== session.user.id) {
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
if (isUniqueConstraintViolation(error, "username")) {
|
||||
return NextResponse.json(
|
||||
{ error: "username_taken", message: "This username is already taken" },
|
||||
{ status: 400 }
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Update user
|
||||
const user = await db.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: {
|
||||
name,
|
||||
username,
|
||||
avatar: avatar || null,
|
||||
bio: bio || null,
|
||||
customLinks: customLinks && customLinks.length > 0 ? customLinks : Prisma.DbNull,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
bio: true,
|
||||
customLinks: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
console.error("Update profile error:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -130,7 +130,7 @@ export default async function BookHomePage() {
|
||||
{/* JSON-LD structured data */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }}
|
||||
/>
|
||||
<div className="max-w-2xl">
|
||||
{/* Continue Reading Button */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Server, Database, Key, Palette, Globe, Settings, Cpu } from "lucide-react";
|
||||
import DeepWikiIcon from "@/../public/deepwiki.svg";
|
||||
import Context7Icon from "@/../public/context7.svg";
|
||||
@@ -17,7 +18,9 @@ export const metadata = {
|
||||
description: "Deploy your own prompts.chat instance with customizable branding, themes, and authentication",
|
||||
};
|
||||
|
||||
export default function SelfHostingPage() {
|
||||
export default async function SelfHostingPage() {
|
||||
const t = await getTranslations("selfHosting");
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl py-10">
|
||||
<h1 className="text-2xl font-bold mb-2">Self-Hosting Guide</h1>
|
||||
@@ -110,9 +113,9 @@ export default function SelfHostingPage() {
|
||||
Prerequisites
|
||||
</h3>
|
||||
<ul className="list-disc list-inside text-muted-foreground space-y-1">
|
||||
<li>Node.js 18+</li>
|
||||
<li>PostgreSQL database</li>
|
||||
<li>npm or yarn</li>
|
||||
<li>{t("prerequisites.nodeJs")}</li>
|
||||
<li>{t("prerequisites.postgresql")}</li>
|
||||
<li>{t("prerequisites.npm")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -802,73 +802,3 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ezoic privacy policy container styling */
|
||||
#ezoic-privacy-policy-container {
|
||||
margin-top: 1rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container h2,
|
||||
#ezoic-privacy-policy-container h3,
|
||||
#ezoic-privacy-policy-container h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container p {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.7;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container a {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container ul,
|
||||
#ezoic-privacy-policy-container ol {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 1.5rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container li {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container th,
|
||||
#ezoic-privacy-policy-container td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#ezoic-privacy-policy-container th {
|
||||
background: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Ezoic ad content: center and constrain injected ad creatives */
|
||||
.ezoic-ad-content * {
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Header } from "@/components/layout/header";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { CookieConsentBanner } from "@/components/layout/cookie-consent";
|
||||
import { Analytics } from "@/components/layout/analytics";
|
||||
import { EzoicAds } from "@/components/layout/ezoic-ads";
|
||||
import { WebsiteStructuredData } from "@/components/seo/structured-data";
|
||||
import { AppBanner } from "@/components/layout/app-banner";
|
||||
import { AnnouncementBanner } from "@/components/layout/announcement-banner";
|
||||
import { LocaleDetector } from "@/components/providers/locale-detector";
|
||||
import { getConfig } from "@/lib/config";
|
||||
import { isRtlLocale } from "@/lib/i18n/config";
|
||||
@@ -153,6 +153,7 @@ export default async function RootLayout({
|
||||
const pathname = headersList.get("x-pathname") || headersList.get("x-invoke-path") || "";
|
||||
const isEmbedRoute = pathname.startsWith("/embed");
|
||||
const isKidsRoute = pathname.startsWith("/kids");
|
||||
const isPresentationRoute = pathname.startsWith("/presentation");
|
||||
|
||||
const locale = await getLocale();
|
||||
const messages = await getMessages();
|
||||
@@ -190,14 +191,14 @@ export default async function RootLayout({
|
||||
{process.env.GOOGLE_ANALYTICS_ID && (
|
||||
<Analytics gaId={process.env.GOOGLE_ANALYTICS_ID} />
|
||||
)}
|
||||
{process.env.EZOIC_ENABLED === "true" && <EzoicAds />}
|
||||
<Providers locale={locale} messages={messages} theme={config.theme} branding={{ ...config.branding, useCloneBranding: config.homepage?.useCloneBranding }}>
|
||||
{isEmbedRoute || isKidsRoute ? (
|
||||
{isEmbedRoute || isKidsRoute || isPresentationRoute ? (
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
<LocaleDetector />
|
||||
<div className="relative min-h-screen flex flex-col">
|
||||
<AnnouncementBanner />
|
||||
<Header authProvider={config.auth.provider} allowRegistration={config.auth.allowRegistration} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
|
||||
@@ -72,7 +72,7 @@ export default async function HomePage() {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute top-0 end-0 bottom-0 w-1/3 2xl:w-1/2 hidden md:block pointer-events-none">
|
||||
<div className="absolute top-0 end-0 bottom-0 w-2/5 xl:w-[45%] 2xl:w-1/2 hidden md:block pointer-events-none overflow-hidden">
|
||||
{/* Video background */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute inset-0 bg-gradient-to-r rtl:bg-gradient-to-l from-background via-background/80 to-transparent z-10" />
|
||||
@@ -126,7 +126,7 @@ export default async function HomePage() {
|
||||
)}
|
||||
|
||||
<div className="container relative z-20">
|
||||
<div className="max-w-2xl">
|
||||
<div className="max-w-2xl lg:max-w-xl xl:max-w-2xl">
|
||||
{useCloneBranding ? (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl md:text-4xl lg:text-5xl !text-2xl sm:!text-3xl md:!text-4xl lg:!text-5xl text-primary">
|
||||
@@ -313,6 +313,24 @@ export default async function HomePage() {
|
||||
/>
|
||||
</BuiltWithLink>
|
||||
<span>by Cognition</span>
|
||||
<span className="mx-0.5">·</span>
|
||||
<span>hosted on</span>
|
||||
<Link href="https://vercel.com/oss" target="_blank" rel="noopener noreferrer" className="inline-flex items-center opacity-60 hover:opacity-100 transition-opacity">
|
||||
<Image
|
||||
src="/sponsors/vercel-oss.svg"
|
||||
alt="Vercel OSS Program"
|
||||
width={240}
|
||||
height={24}
|
||||
className="h-4.5 w-auto dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/sponsors/vercel-oss-dark.svg"
|
||||
alt="Vercel OSS Program"
|
||||
width={240}
|
||||
height={24}
|
||||
className="h-4.5 w-auto hidden dark:block"
|
||||
/>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ChevronLeft, ChevronRight, RadioTower, WifiOff } from "lucide-react";
|
||||
|
||||
type DeckState = {
|
||||
currentSlide: number;
|
||||
totalSlides: number;
|
||||
notes: string[];
|
||||
};
|
||||
|
||||
export default function PresenterNotesPage() {
|
||||
const channelRef = useRef<BroadcastChannel | null>(null);
|
||||
const [state, setState] = useState<DeckState | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
|
||||
const send = useCallback((message: Record<string, unknown>) => {
|
||||
channelRef.current?.postMessage(message);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || typeof BroadcastChannel === "undefined") return;
|
||||
|
||||
const channel = new BroadcastChannel("prompts-presentation");
|
||||
channelRef.current = channel;
|
||||
|
||||
channel.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
if (data?.type === "state") {
|
||||
setConnected(true);
|
||||
setState({
|
||||
currentSlide: data.currentSlide ?? 0,
|
||||
totalSlides: data.totalSlides ?? 0,
|
||||
notes: Array.isArray(data.notes) ? data.notes : [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
channel.postMessage({ type: "request" });
|
||||
const retry = setInterval(() => {
|
||||
if (!channelRef.current) return;
|
||||
channelRef.current.postMessage({ type: "request" });
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(retry);
|
||||
channel.close();
|
||||
channelRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "ArrowRight" || event.key === " " || event.key === "PageDown") {
|
||||
event.preventDefault();
|
||||
send({ type: "next" });
|
||||
} else if (event.key === "ArrowLeft" || event.key === "PageUp") {
|
||||
event.preventDefault();
|
||||
send({ type: "prev" });
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [send]);
|
||||
|
||||
const current = state?.currentSlide ?? 0;
|
||||
const total = state?.totalSlides ?? 0;
|
||||
const notes = state?.notes ?? [];
|
||||
const currentNote = notes[current];
|
||||
const nextNote = notes[current + 1];
|
||||
const atStart = current <= 0;
|
||||
const atEnd = total > 0 && current >= total - 1;
|
||||
|
||||
return (
|
||||
<div className="flex h-[100dvh] w-screen flex-col bg-zinc-950 text-zinc-100">
|
||||
<header className="flex items-center justify-between border-b border-white/10 px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-bold uppercase tracking-[0.28em] text-primary">Presenter notes</span>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ${
|
||||
connected ? "bg-emerald-500/15 text-emerald-300" : "bg-amber-500/15 text-amber-300"
|
||||
}`}
|
||||
>
|
||||
{connected ? <RadioTower className="h-3.5 w-3.5" /> : <WifiOff className="h-3.5 w-3.5" />}
|
||||
{connected ? "Synced" : "Waiting for deck…"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/15 px-4 py-1.5 text-sm font-semibold text-zinc-300">
|
||||
{total > 0 ? `Slide ${current + 1} / ${total}` : "—"}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex flex-1 flex-col gap-6 overflow-hidden p-6 sm:p-8">
|
||||
<section className="flex min-h-0 flex-1 flex-col rounded-3xl border border-white/10 bg-white/[0.03] p-6 sm:p-8">
|
||||
<div className="mb-4 text-xs font-bold uppercase tracking-[0.24em] text-primary">Now</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-2">
|
||||
{connected ? (
|
||||
<p className="text-2xl font-medium leading-relaxed text-zinc-100 sm:text-3xl">
|
||||
{currentNote || "No note for this slide."}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xl text-zinc-400">
|
||||
Open the presentation in another window and this view will sync automatically.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-3xl border border-white/10 bg-white/[0.02] p-5 sm:p-6">
|
||||
<div className="mb-2 text-xs font-bold uppercase tracking-[0.24em] text-zinc-500">Next</div>
|
||||
<p className="line-clamp-3 text-base leading-relaxed text-zinc-400 sm:text-lg">
|
||||
{atEnd ? "End of deck." : nextNote || "—"}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="flex items-center justify-center gap-5 border-t border-white/10 px-6 py-5">
|
||||
<button
|
||||
onClick={() => send({ type: "prev" })}
|
||||
disabled={atStart}
|
||||
className="inline-flex h-12 w-12 items-center justify-center rounded-full border border-white/15 bg-white/5 text-zinc-200 transition hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<span className="min-w-[5rem] text-center text-sm font-medium text-zinc-400">
|
||||
{total > 0 ? `${current + 1} / ${total}` : "—"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => send({ type: "next" })}
|
||||
disabled={atEnd}
|
||||
className="inline-flex h-12 w-12 items-center justify-center rounded-full border border-white/15 bg-white/5 text-zinc-200 transition hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,233 +1,429 @@
|
||||
import { SlideDeck, SlideTitle, SlideContent, SlideHighlight } from "@/components/presentation/SlideDeck";
|
||||
import { MoveRight, Star, GitMerge, FileCode2, Sparkles, MessageSquare, Globe, Users, Code, Zap, Lightbulb } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ArrowRight,
|
||||
Bot,
|
||||
BookOpen,
|
||||
Boxes,
|
||||
Code2,
|
||||
GitBranch,
|
||||
Gamepad2,
|
||||
Globe,
|
||||
Layers3,
|
||||
Lock,
|
||||
MessageSquareText,
|
||||
Network,
|
||||
PackageCheck,
|
||||
Rocket,
|
||||
Search,
|
||||
Share2,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Star,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import type { ReactNode } from "react";
|
||||
import { SlideContent, SlideDeck, SlideHighlight, SlideTitle } from "@/components/presentation/SlideDeck";
|
||||
|
||||
export const metadata = {
|
||||
title: "Why prompts.chat? | Presentation",
|
||||
description: "Discover why prompts are more important than ever in the agentic era.",
|
||||
title: "prompts.chat: An Open Prompt Ecosystem for the Age of Agents | Microsoft Build",
|
||||
description:
|
||||
"15-minute lightning talk at Microsoft Build: how prompts.chat grew from a GitHub repo into an open prompt ecosystem — platform, book, and game — for the age of agents.",
|
||||
};
|
||||
|
||||
const speakerNotes = [
|
||||
"Okay, hi everyone. So I want to start with a mistake that, honestly, a lot of people make. They think that when AI models get smarter, prompts will not matter anymore — like, prompting is just a temporary trick and the next model will make it useless. But actually, for me, it's the opposite. Even the best models give you better answers when you give them better prompts. And in the age of agents, prompts are kind of like atoms — every agent is basically made of them. So no, prompts are not going away. They actually matter more than before. So in the next fifteen minutes, I want to show you why, and the open ecosystem I'm building around them: prompts.chat.",
|
||||
"But first, a quick hello. I'm Fatih. I'm a developer advocate on the WordPress team at Automattic. I also created the Awesome ChatGPT Prompts project, and I'm a GitHub Star. Okay, that's enough about me — let's actually talk about prompts.",
|
||||
"So, this whole thing actually started as a tiny GitHub repo. Just a simple list of prompts you could copy and paste. And one detail people are always surprised by — this is not new at all. I created it almost four years ago, literally about one week after OpenAI announced ChatGPT. The book came later. Honestly, back then I didn't expect much. But it kind of exploded — it became one of the most starred AI projects on GitHub, used by tens of millions of people. And that told me something: a good prompt is actually worth sharing.",
|
||||
"But here's the problem, right? Most prompts just get lost. They're hidden in screenshots, random gists, old chat history. No version, no owner, no review. That great prompt you wrote last week? It's probably gone the second you closed the tab. And okay, that's annoying for us as people — but honestly, it completely falls apart once agents are involved.",
|
||||
"So why does it fall apart for agents? Well, when an agent acts for you, the prompt is not just a casual message anymore. It's actually the plan. It's the contract. It's basically the source code for how that agent behaves. Look at this one — a role, a goal, the sources it can use, the limits, and when to stop. You really want to save and improve something like this, right? Not lose it somewhere in a chat.",
|
||||
"So this is exactly what prompts.chat does. It treats prompts as real, shareable things. You can discover good ones, fork them, keep versions, reuse them. And it's open source, so you can even host it yourself — your team actually owns its own prompt library.",
|
||||
"Now, why does being open matter so much, especially for agents? I'd say three reasons. First, you can actually read what the agent was told to do. Second, the same prompt works across different models and tools. And third, open prompts plug right into agents, editors, an MCP layer. So basically, a world full of agents needs an open world of prompts.",
|
||||
"Okay, now two things people don't really expect. The first one is a book. It's called The Interactive Book of Prompting. It's free, and it's not some boring PDF — it's actually interactive, with live demos you can try right there on the page, from the basics all the way to agents.",
|
||||
"And the second one is a game. So, this is Promi — a little robot — on an adventure through Prompt Land. It's a fun way for kids to learn how to talk to AI. Because, honestly, this skill shouldn't only be for engineers, right? It kind of belongs to everyone, including the next generation.",
|
||||
"So if you put it all together, you actually get one open loop. The repo where it all began, the platform to discover and reuse, the book to learn, the game to bring in new people, and an API and MCP layer that feeds everything back to both humans and agents. So good prompts come in, the community makes them better, and they go back out.",
|
||||
"Okay, so here's the big picture. As agents start doing real work, the prompt is how we steer them — it's becoming the interface to these systems. Now, around those prompts you need some tooling — a way to manage them, version them, test them, serve them. And a lot of people now call this the \"harness\". So, just to be clear, the harness is not the prompt itself — it's actually the tooling that keeps your prompts accurate and well-tuned. And honestly, the same model can give you very different results depending on how good that harness is. So let's keep this whole thing open, shared, and easy to improve — together.",
|
||||
"And that's pretty much it. So, come build this open prompt ecosystem with us. Check out prompts.chat, read the book, play the game with your kids, and take a look at the project on GitHub. Thank you so much — and I think we have a moment for maybe one question.",
|
||||
];
|
||||
|
||||
type CardProps = {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
accent?: string;
|
||||
};
|
||||
|
||||
function PixelEdge() {
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-1.5"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"repeating-linear-gradient(90deg, #F25022 0 7px, #FFB900 7px 14px, #7FBA00 14px 21px, #00A4EF 21px 28px)",
|
||||
}}
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ icon: Icon, title, children, accent = "text-primary" }: CardProps) {
|
||||
return (
|
||||
<div className="relative border border-white/12 bg-white/[0.03] p-6 pt-7 shadow-sm backdrop-blur transition-transform duration-300 hover:-translate-y-1">
|
||||
<PixelEdge />
|
||||
<div className={`mb-4 flex h-12 w-12 items-center justify-center bg-primary/10 ${accent}`}>
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-2xl font-bold text-foreground">{title}</h3>
|
||||
<p className="text-lg leading-relaxed text-muted-foreground">{children}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Pill({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="inline-flex items-center rounded-none border border-primary/40 bg-primary/10 px-4 py-2 font-mono text-sm font-semibold uppercase tracking-[0.18em] text-primary">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CodePanel({ children, title = "prompt.md" }: { children: ReactNode; title?: string }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-none border border-border bg-zinc-950 text-zinc-100 shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-5 py-3">
|
||||
<div className="flex gap-2">
|
||||
<span className="h-3 w-3 rounded-none bg-red-400" />
|
||||
<span className="h-3 w-3 rounded-none bg-yellow-400" />
|
||||
<span className="h-3 w-3 rounded-none bg-green-400" />
|
||||
</div>
|
||||
<span className="font-mono text-xs text-zinc-400">{title}</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap p-6 font-mono text-sm leading-relaxed md:text-base">{children}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowStep({ icon: Icon, title, text }: { icon: LucideIcon; title: string; text: string }) {
|
||||
return (
|
||||
<div className="relative border border-white/12 bg-white/[0.03] p-5 pt-6 shadow-sm">
|
||||
<PixelEdge />
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center bg-primary/10 text-primary">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold">{title}</h3>
|
||||
<p className="mt-2 text-lg leading-relaxed text-muted-foreground">{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PresentationPage() {
|
||||
return (
|
||||
<SlideDeck>
|
||||
{/* 1. Title Slide */}
|
||||
<div className="flex flex-col items-center justify-center text-center h-full">
|
||||
<div className="w-24 h-24 bg-primary/10 rounded-3xl flex items-center justify-center mb-8 rotate-12 transition-transform hover:rotate-0 duration-500">
|
||||
<Sparkles className="w-12 h-12 text-primary" />
|
||||
<SlideDeck notes={speakerNotes}>
|
||||
<div className="flex min-h-[70vh] flex-col items-center justify-center text-center">
|
||||
<Image
|
||||
src="/presentation/build-logo.png"
|
||||
alt="Microsoft Build"
|
||||
width={720}
|
||||
height={405}
|
||||
priority
|
||||
unoptimized
|
||||
className="mb-10 h-auto w-[20rem] md:w-[24rem]"
|
||||
/>
|
||||
<div className="mb-7 flex items-center justify-center">
|
||||
<Image
|
||||
src="/logo-dark.svg"
|
||||
alt="prompts.chat logo"
|
||||
width={112}
|
||||
height={112}
|
||||
unoptimized
|
||||
className="h-24 w-24 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<SlideTitle className="mb-6">Why prompts.chat?</SlideTitle>
|
||||
<SlideContent className="max-w-3xl mb-12">Discover why prompts are more important than ever in the agentic era.</SlideContent>
|
||||
<p className="text-sm text-muted-foreground animate-pulse mt-8">
|
||||
Use arrow keys <MoveRight className="inline w-4 h-4 mx-1" /> or swipe to navigate
|
||||
<SlideTitle className="max-w-5xl">prompts.chat</SlideTitle>
|
||||
<SlideContent className="mx-auto max-w-4xl">
|
||||
Building an <SlideHighlight>Open Prompt Ecosystem</SlideHighlight> for the Age of Agents
|
||||
</SlideContent>
|
||||
<p className="mx-auto mt-6 max-w-3xl text-lg font-medium text-muted-foreground/80 md:text-xl">
|
||||
Smarter models don't make prompts obsolete — prompts are the{" "}
|
||||
<SlideHighlight>atoms of every agent</SlideHighlight>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 2. The Genesis */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<Star className="w-12 h-12 text-yellow-500 mb-6" />
|
||||
<SlideTitle>The History of prompts.chat</SlideTitle>
|
||||
<SlideContent>
|
||||
From a simple repository to a full-fledged platform. We started as Awesome ChatGPT Prompts, and evolved to support the entire AI community with typed prompts, workflows, and skills.
|
||||
</SlideContent>
|
||||
<div className="mt-12 p-6 bg-muted/50 rounded-2xl border border-border max-w-2xl">
|
||||
<p className="text-lg text-foreground font-mono">
|
||||
Awesome ChatGPT Prompts → <SlideHighlight>prompts.chat</SlideHighlight>
|
||||
<div className="grid min-h-[70vh] items-center gap-8 lg:grid-cols-[1fr_0.95fr]">
|
||||
<div className="relative order-2 lg:order-1">
|
||||
<div className="absolute -left-8 top-10 h-72 w-72 rounded-none bg-primary/20 blur-3xl" />
|
||||
<div className="relative mx-auto w-full max-w-[34rem]">
|
||||
<div className="absolute -inset-4 -rotate-3 rounded-none bg-gradient-to-br from-primary/30 via-primary/10 to-transparent" />
|
||||
<div className="relative overflow-hidden rounded-none border border-border bg-background shadow-2xl">
|
||||
<Image
|
||||
src="https://github.com/f.png"
|
||||
alt="Fatih Kadir Akın profile photo"
|
||||
width={900}
|
||||
height={900}
|
||||
priority
|
||||
unoptimized
|
||||
className="aspect-square w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 via-black/45 to-transparent p-8 text-white">
|
||||
<h2 className="mt-2 text-5xl font-black tracking-tight">Fatih Kadir Akın</h2>
|
||||
<p className="mt-3 max-w-md text-lg font-semibold leading-snug text-white/85">
|
||||
Developer Advocate, WordPress @ Automattic
|
||||
<br /> Creator of prompts.chat
|
||||
<br /> GitHub Star
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2">
|
||||
<SlideTitle className="mb-5">Hi, I'm Fatih.</SlideTitle>
|
||||
<SlideContent className="max-w-2xl">
|
||||
I build tools that make developers' lives easier — one of them is{" "}
|
||||
<SlideHighlight>prompts.chat</SlideHighlight>.
|
||||
</SlideContent>
|
||||
<div className="mt-10 flex flex-wrap items-center gap-5">
|
||||
<div className="flex items-center gap-3 rounded-none border border-border/70 bg-background/75 px-5 py-4 shadow-sm backdrop-blur">
|
||||
<Image
|
||||
src="https://cdn.simpleicons.org/automattic/FFFFFF"
|
||||
alt="Automattic"
|
||||
width={40}
|
||||
height={40}
|
||||
unoptimized
|
||||
className="h-10 w-10"
|
||||
/>
|
||||
<span className="text-xl font-bold text-foreground">Automattic</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-none border border-border/70 bg-background/75 px-5 py-4 shadow-sm backdrop-blur">
|
||||
<Star className="h-9 w-9 text-yellow-500" />
|
||||
<span className="text-xl font-bold text-foreground">GitHub Star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-[70vh] items-center gap-10 lg:grid-cols-2">
|
||||
<div>
|
||||
<SlideTitle>It started as a GitHub repo</SlideTitle>
|
||||
<SlideContent>
|
||||
<SlideHighlight>Awesome ChatGPT Prompts</SlideHighlight> — a simple list of copy-paste prompts that grew
|
||||
into one of the most-starred AI repositories in the world.
|
||||
</SlideContent>
|
||||
<p className="mt-6 max-w-2xl text-lg font-medium text-muted-foreground/80 md:text-xl">
|
||||
And it's not new — the repo (and later the book) are almost{" "}
|
||||
<SlideHighlight>four years old</SlideHighlight>, born just a week after OpenAI announced ChatGPT.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. The Paradigm Shift */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<Zap className="w-12 h-12 text-blue-500 mb-6" />
|
||||
<SlideTitle>The Agentic Era</SlideTitle>
|
||||
<SlideContent>
|
||||
Why are prompts still important? Because agents need instructions. As AI becomes more autonomous, the quality of instructions determines the quality of the outcome.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 4. The Core Question */}
|
||||
<div className="flex flex-col items-center justify-center text-center h-full">
|
||||
<Lightbulb className="w-16 h-16 text-yellow-400 mb-8" />
|
||||
<SlideTitle className="text-5xl md:text-6xl">Wait, isn't AI getting smarter?</SlideTitle>
|
||||
<SlideContent className="max-w-4xl mt-8">
|
||||
Yes, but <SlideHighlight>smart AI still needs direction.</SlideHighlight>
|
||||
<br /><br />
|
||||
A genius without instructions is just a very capable idle machine.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 5. The Agentic Need */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<SlideTitle>The Agentic Need</SlideTitle>
|
||||
<SlideContent>
|
||||
Agents require precise instructions, constraints, and goal definitions.
|
||||
<ul className="mt-8 space-y-6 list-disc list-inside ml-4 text-foreground/80 text-2xl md:text-3xl">
|
||||
<li>What tools can they use?</li>
|
||||
<li>What are their boundaries?</li>
|
||||
<li>How should they format the output?</li>
|
||||
<li>What tone should they adopt?</li>
|
||||
</ul>
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 6. The SLM Revolution */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<Globe className="w-12 h-12 text-green-500 mb-6" />
|
||||
<SlideTitle>Crucial for SLMs</SlideTitle>
|
||||
<SlideContent>
|
||||
Small Language Models (SLMs) are the future of edge computing. They require precise, well-crafted prompts to perform tasks accurately without the massive parameter count of frontier models.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 7. SLM Constraints */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<SlideTitle>SLM Constraints</SlideTitle>
|
||||
<SlideContent>
|
||||
Smaller models lack the vast "common sense" of 1T+ parameter models.
|
||||
<br /><br />
|
||||
They require <SlideHighlight>highly optimized, battle-tested prompts</SlideHighlight> to punch above their weight class and run efficiently on local devices.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 8. The New Primitive */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<FileCode2 className="w-12 h-12 text-purple-500 mb-6" />
|
||||
<SlideTitle>Prompts are New Code Snippets</SlideTitle>
|
||||
<SlideContent>
|
||||
Just as developers copy/paste code snippets from StackOverflow, AI practitioners now share prompt templates. Prompts are the new primitive of programming.
|
||||
</SlideContent>
|
||||
<div className="mt-12 flex items-center gap-4 text-xl md:text-2xl font-mono text-muted-foreground bg-muted p-6 rounded-xl border max-w-fit">
|
||||
<span>const codeSnippet = "..."</span>
|
||||
<MoveRight className="w-6 h-6 text-primary mx-2" />
|
||||
<span className="text-primary">const promptTemplate = "..."</span>
|
||||
<div className="grid gap-4">
|
||||
<Card icon={Star} title="Hundreds of thousands of stars" accent="text-yellow-500">
|
||||
One of GitHub's most popular AI projects.
|
||||
</Card>
|
||||
<Card icon={Globe} title="Tens of millions of developers">
|
||||
People copied these prompts into every AI tool imaginable.
|
||||
</Card>
|
||||
<Card icon={MessageSquareText} title="A new kind of artifact">
|
||||
The prompt itself became something worth sharing.
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 9. Open Source AI */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<Users className="w-12 h-12 text-indigo-500 mb-6" />
|
||||
<SlideTitle>Community Sharing</SlideTitle>
|
||||
<SlideContent>
|
||||
Nobody learns in isolation. By sharing prompts, we accelerate the collective understanding of how to communicate with AI. It's the open-source movement for human-AI interaction.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 10. Collective Intelligence */}
|
||||
<div className="flex flex-col items-center justify-center text-center h-full">
|
||||
<SlideTitle className="text-5xl md:text-6xl">Collective Intelligence</SlideTitle>
|
||||
<SlideContent className="max-w-4xl mt-8">
|
||||
We are building the <SlideHighlight>largest open repository</SlideHighlight> of human-AI interaction patterns.
|
||||
<br /><br />
|
||||
Tested, versioned, and peer-reviewed.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 11. Feature - Typed Prompts */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<Code className="w-12 h-12 text-red-500 mb-6" />
|
||||
<SlideTitle>Feature: Typed Prompts</SlideTitle>
|
||||
<SlideContent>
|
||||
Natural language is ambiguous. <SlideHighlight>Typed Prompts</SlideHighlight> bring structure.
|
||||
<br /><br />
|
||||
Define inputs, outputs, and variables systematically so prompts can be executed predictably via APIs.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 12. Feature - Workflows */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<div className="flex gap-2 mb-6">
|
||||
<div className="w-8 h-8 rounded bg-primary"></div>
|
||||
<MoveRight className="w-8 h-8 text-muted-foreground" />
|
||||
<div className="w-8 h-8 rounded bg-primary/70"></div>
|
||||
<MoveRight className="w-8 h-8 text-muted-foreground" />
|
||||
<div className="w-8 h-8 rounded bg-primary/40"></div>
|
||||
<div className="flex min-h-[70vh] flex-col justify-center">
|
||||
<SlideTitle>But prompts get lost</SlideTitle>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card icon={Share2} title="Scattered" accent="text-orange-500">
|
||||
Trapped in screenshots, gists, and DMs.
|
||||
</Card>
|
||||
<Card icon={GitBranch} title="Unversioned" accent="text-sky-500">
|
||||
No history, no review, no ownership.
|
||||
</Card>
|
||||
<Card icon={Lock} title="Locked in chats" accent="text-red-500">
|
||||
The good ones disappear when the tab closes.
|
||||
</Card>
|
||||
</div>
|
||||
<SlideTitle>Feature: Workflows</SlideTitle>
|
||||
<SlideContent>
|
||||
Some tasks are too complex for a single prompt.
|
||||
<br /><br />
|
||||
Chain multiple prompts together to create <SlideHighlight>Workflows</SlideHighlight> — dividing and conquering complex agentic tasks.
|
||||
<SlideContent className="mt-10 max-w-4xl">
|
||||
Annoying for humans — and <SlideHighlight>broken for agents</SlideHighlight>.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 13. Feature - Agent Skills */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<SlideTitle>Feature: Agent Skills</SlideTitle>
|
||||
<SlideContent>
|
||||
Equip coding assistants (like Claude, Cursor, Windsurf) with specialized, multi-file capabilities.
|
||||
<br /><br />
|
||||
Give your AI the context and rules it needs to build within your specific tech stack.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 14. Feature - Change Requests */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<GitMerge className="w-12 h-12 text-orange-500 mb-6" />
|
||||
<SlideTitle>Feature: Change Requests</SlideTitle>
|
||||
<SlideContent>
|
||||
Prompts evolve. We've introduced <SlideHighlight>Pull Requests for Prompts.</SlideHighlight>
|
||||
<br /><br />
|
||||
Collaborate, suggest improvements, and refine instructions together as a community.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 15. For Developers */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<SlideTitle>For Developers</SlideTitle>
|
||||
<SlideContent>
|
||||
prompts.chat isn't just for end-users. With features like Agent Skills, Typed Prompts, and API integrations, it's a vital tool for developers building the next generation of AI apps.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 16. For Everyone */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<MessageSquare className="w-12 h-12 text-pink-500 mb-6" />
|
||||
<SlideTitle>For Everyone</SlideTitle>
|
||||
<SlideContent>
|
||||
Discover, collect, and learn from <SlideHighlight>Promptmasters</SlideHighlight>.
|
||||
<br /><br />
|
||||
A beautifully designed, accessible platform to find exactly what you need to make AI work for you.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 17. Self-Hosted */}
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<Globe className="w-12 h-12 text-teal-500 mb-6" />
|
||||
<SlideTitle>Fully Self-Hosted</SlideTitle>
|
||||
<SlideContent>
|
||||
Your data, your platform.
|
||||
<br /><br />
|
||||
prompts.chat is <SlideHighlight>100% open-source</SlideHighlight> and deployable to any server or cloud provider. Own your organization's prompt library securely.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 18. The Vision */}
|
||||
<div className="flex flex-col items-center justify-center text-center h-full">
|
||||
<SlideTitle className="text-5xl md:text-6xl">The Vision</SlideTitle>
|
||||
<SlideContent className="max-w-4xl mt-8">
|
||||
To create the <SlideHighlight>standard protocol</SlideHighlight> for human-AI interaction.
|
||||
<br /><br />
|
||||
Where every great instruction is shared, improved, and accessible to all.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
{/* 19. Call to Action */}
|
||||
<div className="flex flex-col items-center justify-center text-center h-full">
|
||||
<div className="w-24 h-24 bg-primary/10 rounded-full flex items-center justify-center mb-8">
|
||||
<Users className="w-12 h-12 text-primary" />
|
||||
<div className="grid min-h-[70vh] items-center gap-8 lg:grid-cols-2">
|
||||
<div>
|
||||
<SlideTitle>In the age of agents, a prompt is a spec</SlideTitle>
|
||||
<SlideContent>
|
||||
When an agent acts on your behalf, the prompt stops being a casual message. It becomes the{" "}
|
||||
<SlideHighlight>contract</SlideHighlight> — the source code of behavior.
|
||||
</SlideContent>
|
||||
</div>
|
||||
<SlideTitle>Join the Community</SlideTitle>
|
||||
<SlideContent className="max-w-2xl mb-12">
|
||||
Start exploring, sharing, and building the future of AI interaction today.
|
||||
<CodePanel title="agent-instructions.md">
|
||||
{`Role: Release notes agent for our repo.
|
||||
Goal: Summarize merged PRs since the last tag.
|
||||
Sources: GitHub API, CHANGELOG.md.
|
||||
Limits: No breaking-change claims without a label.
|
||||
Stop: When the draft is posted for review.
|
||||
Output: Markdown, grouped by feature / fix / chore.`}
|
||||
</CodePanel>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-[70vh] items-center gap-10 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<div>
|
||||
<SlideTitle>prompts.chat: an open prompt ecosystem</SlideTitle>
|
||||
<SlideContent>
|
||||
Prompts as <SlideHighlight>first-class assets</SlideHighlight> — discoverable, forkable, versioned, and
|
||||
reusable. Open source and self-hostable.
|
||||
</SlideContent>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Card icon={Search} title="Discover">
|
||||
Find a proven prompt instead of starting from scratch.
|
||||
</Card>
|
||||
<Card icon={GitBranch} title="Fork">
|
||||
Branch it, tweak it, make it yours.
|
||||
</Card>
|
||||
<Card icon={Layers3} title="Version">
|
||||
Prompts mature through review and feedback.
|
||||
</Card>
|
||||
<Card icon={PackageCheck} title="Reuse">
|
||||
Carry quality across teams, tools, and agents.
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[70vh] flex-col justify-center">
|
||||
<SlideTitle>Why open matters for agents</SlideTitle>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card icon={ShieldCheck} title="Inspectable" accent="text-emerald-500">
|
||||
You can read exactly what the agent was told to do.
|
||||
</Card>
|
||||
<Card icon={Globe} title="Portable" accent="text-sky-500">
|
||||
The same instruction travels across models and tools.
|
||||
</Card>
|
||||
<Card icon={Bot} title="Composable" accent="text-purple-500">
|
||||
Open prompts plug into agents, IDEs, and an MCP layer.
|
||||
</Card>
|
||||
</div>
|
||||
<SlideContent className="mt-10">
|
||||
An ecosystem of agents needs an <SlideHighlight>open</SlideHighlight> ecosystem of prompts.
|
||||
</SlideContent>
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Link href="/discover">
|
||||
<Button size="lg" className="text-lg px-8 py-6 rounded-xl">Explore Prompts</Button>
|
||||
</Link>
|
||||
<Link href="https://github.com/fka/awesome-chatgpt-prompts" target="_blank">
|
||||
<Button size="lg" variant="outline" className="text-lg px-8 py-6 rounded-xl">Star on GitHub</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-[70vh] items-center gap-10 lg:grid-cols-2">
|
||||
<div>
|
||||
<SlideTitle>Learn it: The Interactive Book of Prompting</SlideTitle>
|
||||
<SlideContent>
|
||||
A <SlideHighlight>free, interactive</SlideHighlight> prompt-engineering guide — with live, hands-on demos
|
||||
instead of a static PDF.
|
||||
</SlideContent>
|
||||
<div className="mt-7 space-y-3">
|
||||
<div className="flex items-center gap-3 text-lg text-muted-foreground">
|
||||
<BookOpen className="h-5 w-5 flex-none text-primary" />
|
||||
Hands-on chapters, from fundamentals to agents.
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-lg text-muted-foreground">
|
||||
<Code2 className="h-5 w-5 flex-none text-primary" />
|
||||
Runnable demos you can try right on the page.
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<Pill>prompts.chat/book</Pill>
|
||||
<Pill>Open & community-driven</Pill>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mx-auto w-full max-w-[34rem]">
|
||||
<div className="absolute -inset-4 -rotate-2 rounded-none bg-gradient-to-br from-primary/30 via-primary/10 to-transparent" />
|
||||
<div className="relative overflow-hidden rounded-none border border-border bg-background shadow-2xl">
|
||||
<Image
|
||||
src="/book-cover-photo.jpg"
|
||||
alt="The Interactive Book of Prompting cover"
|
||||
width={1200}
|
||||
height={630}
|
||||
unoptimized
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-[70vh] items-center gap-10 lg:grid-cols-2">
|
||||
<div>
|
||||
<SlideTitle>Play it: Promi in Prompt Land</SlideTitle>
|
||||
<SlideContent>
|
||||
A <SlideHighlight>game-based</SlideHighlight> way for kids to learn how to talk to AI — because this
|
||||
literacy belongs to everyone.
|
||||
</SlideContent>
|
||||
<div className="mt-7 space-y-3">
|
||||
<div className="flex items-center gap-3 text-lg text-muted-foreground">
|
||||
<Gamepad2 className="h-5 w-5 flex-none text-green-500" />
|
||||
Join Promi the robot on an adventure through Prompt Land.
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-lg text-muted-foreground">
|
||||
<Sparkles className="h-5 w-5 flex-none text-amber-500" />
|
||||
Kids practice asking AI for what they actually want.
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<Pill>prompts.chat/kids</Pill>
|
||||
<Pill>For the next generation</Pill>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mx-auto w-full max-w-[19rem]">
|
||||
<div className="absolute -inset-4 rotate-2 rounded-none bg-gradient-to-br from-primary/30 via-primary/10 to-transparent" />
|
||||
<div className="relative overflow-hidden rounded-none border border-border bg-background shadow-2xl">
|
||||
<Image
|
||||
src="/presentation/kids-screenshot.png"
|
||||
alt="Promi in Prompt Land — the kids game"
|
||||
width={800}
|
||||
height={2026}
|
||||
unoptimized
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[70vh] flex-col justify-center">
|
||||
<SlideTitle>One open loop</SlideTitle>
|
||||
<div className="grid gap-5 md:grid-cols-5">
|
||||
<FlowStep icon={Star} title="Repo" text="Where it all began." />
|
||||
<FlowStep icon={Boxes} title="Platform" text="Discover, fork, version, reuse." />
|
||||
<FlowStep icon={BookOpen} title="Book" text="Learn the craft." />
|
||||
<FlowStep icon={Gamepad2} title="Game" text="Onboard the next generation." />
|
||||
<FlowStep icon={Network} title="API / MCP" text="Feed humans and agents." />
|
||||
</div>
|
||||
<SlideContent className="mt-10 max-w-4xl">
|
||||
Prompts flow in, get refined by the community, and flow back out to{" "}
|
||||
<SlideHighlight>humans and agents</SlideHighlight>.
|
||||
</SlideContent>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[70vh] flex-col items-center justify-center text-center">
|
||||
<div className="mb-8 flex h-24 w-24 items-center justify-center rounded-none bg-primary/10 text-primary">
|
||||
<Workflow className="h-12 w-12" />
|
||||
</div>
|
||||
<SlideTitle className="max-w-5xl">Prompts are the new interface</SlideTitle>
|
||||
<SlideContent className="mx-auto max-w-4xl">
|
||||
As agents take action, the prompt is how we steer them — and the tooling that manages those prompts is what
|
||||
people now call the <SlideHighlight>“harness”</SlideHighlight>.
|
||||
</SlideContent>
|
||||
<p className="mx-auto mt-6 max-w-3xl text-lg font-medium text-muted-foreground/80 md:text-xl">
|
||||
The harness isn't the prompt — it's what keeps your prompts accurate and well-tuned. So let's
|
||||
keep it <SlideHighlight>open, shared, and improvable</SlideHighlight>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[70vh] flex-col items-center justify-center text-center">
|
||||
<div className="mb-8 flex h-24 w-24 items-center justify-center rounded-none bg-primary/10 text-primary">
|
||||
<Rocket className="h-12 w-12" />
|
||||
</div>
|
||||
<SlideTitle>Thank you</SlideTitle>
|
||||
<SlideContent className="mx-auto max-w-4xl">
|
||||
Come build the open prompt ecosystem with us.
|
||||
</SlideContent>
|
||||
<div className="mt-10 flex flex-wrap justify-center gap-3">
|
||||
<Pill>prompts.chat</Pill>
|
||||
<Pill>/book</Pill>
|
||||
<Pill>/kids</Pill>
|
||||
<Pill>github.com/f/prompts.chat</Pill>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center gap-2 text-lg font-semibold text-muted-foreground">
|
||||
Questions <ArrowRight className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</SlideDeck>
|
||||
|
||||
@@ -97,27 +97,6 @@ export default function PrivacyPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{process.env.EZOIC_ENABLED === "true" && (
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-semibold">Advertising & Ezoic</h2>
|
||||
<p className="text-muted-foreground">
|
||||
We use Ezoic to provide personalized ads and analytics on this site. Ezoic and its partners
|
||||
may collect and use data about your visit to this and other websites to provide relevant
|
||||
advertisements. For full details on how Ezoic and its partners use your information, including
|
||||
a list of known cookies used on this site, please see the{" "}
|
||||
<a
|
||||
href="http://g.ezoic.net/privacy/prompts.chat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-foreground"
|
||||
>
|
||||
Ezoic privacy disclosures
|
||||
</a>.
|
||||
</p>
|
||||
<span id="ezoic-privacy-policy-embed"></span>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-semibold">Your Rights & CC0 License</h2>
|
||||
<p className="text-muted-foreground">
|
||||
|
||||
@@ -23,6 +23,8 @@ function getUserIdentifier(user: { email: string; username: string; githubUserna
|
||||
return user.githubUsername || (isUnclaimedAccount ? user.username : user.email);
|
||||
}
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const prompts = await db.prompt.findMany({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import crypto from "crypto";
|
||||
|
||||
function getUserIdentifier(user: {
|
||||
username: string;
|
||||
@@ -8,139 +9,227 @@ function getUserIdentifier(user: {
|
||||
return user.githubUsername || user.username;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const CONTENT_PREVIEW_LENGTH = 500;
|
||||
const DEFAULT_LIMIT = 50;
|
||||
const MAX_LIMIT = 100;
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 3600;
|
||||
|
||||
function generateETag(count: number, latestUpdatedAt: Date | null): string {
|
||||
const raw = `${count}-${latestUpdatedAt?.toISOString() ?? "none"}`;
|
||||
const hash = crypto.createHash("md5").update(raw).digest("hex");
|
||||
return `"${hash}"`;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const prompts = await db.prompt.findMany({
|
||||
where: {
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const fullContent = searchParams.get("full_content") === "true";
|
||||
const pageParam = searchParams.get("page");
|
||||
const limitParam = searchParams.get("limit");
|
||||
|
||||
const isPaginated = pageParam !== null || limitParam !== null;
|
||||
|
||||
const page = Math.max(1, parseInt(pageParam ?? "1", 10) || 1);
|
||||
const limit = Math.min(
|
||||
MAX_LIMIT,
|
||||
Math.max(1, parseInt(limitParam ?? String(DEFAULT_LIMIT), 10) || DEFAULT_LIMIT)
|
||||
);
|
||||
|
||||
const whereClause = {
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
// Fetch total count and latest updatedAt for ETag generation
|
||||
const [totalCount, latestPrompt] = await Promise.all([
|
||||
db.prompt.count({ where: whereClause }),
|
||||
db.prompt.findFirst({
|
||||
where: whereClause,
|
||||
orderBy: { updatedAt: "desc" },
|
||||
select: { updatedAt: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Generate and check ETag for conditional requests
|
||||
const etag = generateETag(totalCount, latestPrompt?.updatedAt ?? null);
|
||||
const ifNoneMatch = request.headers.get("If-None-Match");
|
||||
|
||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||
return new NextResponse(null, {
|
||||
status: 304,
|
||||
headers: {
|
||||
ETag: etag,
|
||||
"Cache-Control":
|
||||
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const selectClause = {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
content: true,
|
||||
type: true,
|
||||
mediaUrl: true,
|
||||
viewCount: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
structuredFormat: true,
|
||||
isFeatured: true,
|
||||
featuredAt: true,
|
||||
requiresMediaUpload: true,
|
||||
requiredMediaType: true,
|
||||
requiredMediaCount: true,
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
content: true,
|
||||
type: true,
|
||||
mediaUrl: true,
|
||||
viewCount: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
structuredFormat: true,
|
||||
isFeatured: true,
|
||||
featuredAt: true,
|
||||
requiresMediaUpload: true,
|
||||
requiredMediaType: true,
|
||||
requiredMediaCount: true,
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
icon: true,
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
contributors: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
contributors: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
githubUsername: true,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
select: {
|
||||
tag: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
votes: true,
|
||||
comments: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
votes: true,
|
||||
comments: true,
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const queryOptions: any = {
|
||||
where: whereClause,
|
||||
select: selectClause,
|
||||
orderBy: { createdAt: "desc" as const },
|
||||
};
|
||||
|
||||
if (isPaginated) {
|
||||
queryOptions.skip = (page - 1) * limit;
|
||||
queryOptions.take = limit;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const prompts: any[] = await db.prompt.findMany(queryOptions);
|
||||
|
||||
const includeFullContent = fullContent;
|
||||
|
||||
const formattedPrompts = prompts.map((prompt) => {
|
||||
const content = prompt.content ?? "";
|
||||
const contentPreview =
|
||||
content.length > CONTENT_PREVIEW_LENGTH
|
||||
? content.slice(0, CONTENT_PREVIEW_LENGTH) + "..."
|
||||
: content;
|
||||
|
||||
return {
|
||||
id: prompt.id,
|
||||
title: prompt.title,
|
||||
slug: prompt.slug,
|
||||
description: prompt.description,
|
||||
...(includeFullContent
|
||||
? { content, contentPreview }
|
||||
: { contentPreview }),
|
||||
type: prompt.type,
|
||||
structuredFormat: prompt.structuredFormat,
|
||||
mediaUrl: prompt.mediaUrl,
|
||||
viewCount: prompt.viewCount,
|
||||
voteCount: prompt._count.votes,
|
||||
commentCount: prompt._count.comments,
|
||||
isFeatured: prompt.isFeatured,
|
||||
featuredAt: prompt.featuredAt,
|
||||
requiresMediaUpload: prompt.requiresMediaUpload,
|
||||
requiredMediaType: prompt.requiredMediaType,
|
||||
requiredMediaCount: prompt.requiredMediaCount,
|
||||
createdAt: prompt.createdAt,
|
||||
updatedAt: prompt.updatedAt,
|
||||
category: prompt.category
|
||||
? {
|
||||
id: prompt.category.id,
|
||||
name: prompt.category.name,
|
||||
slug: prompt.category.slug,
|
||||
icon: prompt.category.icon,
|
||||
}
|
||||
: null,
|
||||
author: {
|
||||
username: prompt.author.username,
|
||||
name: prompt.author.name,
|
||||
avatar: prompt.author.avatar,
|
||||
identifier: getUserIdentifier(prompt.author),
|
||||
verified: prompt.author.verified,
|
||||
},
|
||||
contributors: prompt.contributors.map((c: { username: string; name: string | null; avatar: string | null; githubUsername: string | null; verified: boolean }) => ({
|
||||
username: c.username,
|
||||
name: c.name,
|
||||
avatar: c.avatar,
|
||||
identifier: getUserIdentifier(c),
|
||||
verified: c.verified,
|
||||
})),
|
||||
tags: prompt.tags.map((pt: { tag: { id: string; name: string; slug: string; color: string | null } }) => ({
|
||||
id: pt.tag.id,
|
||||
name: pt.tag.name,
|
||||
slug: pt.tag.slug,
|
||||
color: pt.tag.color,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const formattedPrompts = prompts.map((prompt) => ({
|
||||
id: prompt.id,
|
||||
title: prompt.title,
|
||||
slug: prompt.slug,
|
||||
description: prompt.description,
|
||||
content: prompt.content,
|
||||
type: prompt.type,
|
||||
structuredFormat: prompt.structuredFormat,
|
||||
mediaUrl: prompt.mediaUrl,
|
||||
viewCount: prompt.viewCount,
|
||||
voteCount: prompt._count.votes,
|
||||
commentCount: prompt._count.comments,
|
||||
isFeatured: prompt.isFeatured,
|
||||
featuredAt: prompt.featuredAt,
|
||||
requiresMediaUpload: prompt.requiresMediaUpload,
|
||||
requiredMediaType: prompt.requiredMediaType,
|
||||
requiredMediaCount: prompt.requiredMediaCount,
|
||||
createdAt: prompt.createdAt,
|
||||
updatedAt: prompt.updatedAt,
|
||||
category: prompt.category
|
||||
? {
|
||||
id: prompt.category.id,
|
||||
name: prompt.category.name,
|
||||
slug: prompt.category.slug,
|
||||
icon: prompt.category.icon,
|
||||
}
|
||||
: null,
|
||||
author: {
|
||||
username: prompt.author.username,
|
||||
name: prompt.author.name,
|
||||
avatar: prompt.author.avatar,
|
||||
identifier: getUserIdentifier(prompt.author),
|
||||
verified: prompt.author.verified,
|
||||
},
|
||||
contributors: prompt.contributors.map((c) => ({
|
||||
username: c.username,
|
||||
name: c.name,
|
||||
avatar: c.avatar,
|
||||
identifier: getUserIdentifier(c),
|
||||
verified: c.verified,
|
||||
})),
|
||||
tags: prompt.tags.map((pt) => ({
|
||||
id: pt.tag.id,
|
||||
name: pt.tag.name,
|
||||
slug: pt.tag.slug,
|
||||
color: pt.tag.color,
|
||||
})),
|
||||
}));
|
||||
const totalPages = isPaginated ? Math.ceil(totalCount / limit) : 1;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
count: formattedPrompts.length,
|
||||
prompts: formattedPrompts,
|
||||
const responseBody = isPaginated
|
||||
? {
|
||||
count: totalCount,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
hasMore: page < totalPages,
|
||||
prompts: formattedPrompts,
|
||||
}
|
||||
: {
|
||||
count: formattedPrompts.length,
|
||||
prompts: formattedPrompts,
|
||||
};
|
||||
|
||||
return NextResponse.json(responseBody, {
|
||||
headers: {
|
||||
"Cache-Control":
|
||||
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
|
||||
ETag: etag,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("prompts.json error:", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -8,6 +8,7 @@ export const size = {
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
export const revalidate = 3600;
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
TEXT: "Text Prompt",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AnimatedDate } from "@/components/ui/animated-date";
|
||||
import { ShareDropdown } from "@/components/prompts/share-dropdown";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { canViewPrompt } from "@/lib/prompt-access";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
@@ -61,13 +62,20 @@ export async function generateMetadata({ params }: PromptPageProps): Promise<Met
|
||||
const id = extractPromptId(idParam);
|
||||
const prompt = await db.prompt.findUnique({
|
||||
where: { id },
|
||||
select: { title: true, description: true },
|
||||
select: { title: true, description: true, isPrivate: true, authorId: true },
|
||||
});
|
||||
|
||||
if (!prompt) {
|
||||
return { title: "Prompt Not Found" };
|
||||
}
|
||||
|
||||
if (prompt.isPrivate) {
|
||||
const session = await auth();
|
||||
if (!canViewPrompt(prompt, session)) {
|
||||
return { title: "Prompt Not Found" };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: prompt.title,
|
||||
description: prompt.description || `View the prompt: ${prompt.title}`,
|
||||
@@ -223,7 +231,7 @@ export default async function PromptPage({ params }: PromptPageProps) {
|
||||
}
|
||||
|
||||
// Check if user can view private prompt
|
||||
if (prompt.isPrivate && prompt.authorId !== session?.user?.id) {
|
||||
if (!canViewPrompt(prompt, session)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import { isAISearchEnabled, semanticSearch } from "@/lib/ai/embeddings";
|
||||
import { isAIGenerationEnabled } from "@/lib/ai/generation";
|
||||
import config from "@/../prompts.config";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Prompts",
|
||||
description: "Browse and discover AI prompts",
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { runEzoic } from "@/lib/ezoic";
|
||||
|
||||
interface EzoicPlaceholderProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export function EzoicPlaceholder({ id }: EzoicPlaceholderProps) {
|
||||
const t = useTranslations("common");
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const [hasAdContent, setHasAdContent] = useState(true);
|
||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Render the placeholder div first, then tell Ezoic to fill it.
|
||||
// This ensures the DOM element exists before showAds is called.
|
||||
setIsRendered(true);
|
||||
|
||||
// Use requestAnimationFrame to guarantee the placeholder div
|
||||
// is in the DOM before Ezoic tries to find it.
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
runEzoic(() => {
|
||||
window.ezstandalone?.showAds(id);
|
||||
});
|
||||
});
|
||||
|
||||
// Observe the placeholder for ad content injection by Ezoic.
|
||||
// If nothing is injected after a timeout, hide the container
|
||||
// to avoid showing an empty white card.
|
||||
const el = document.getElementById(`ezoic-pub-ad-placeholder-${id}`);
|
||||
let observer: MutationObserver | undefined;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
if (el) {
|
||||
let filled = false;
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
if (el.childNodes.length > 0 || el.innerHTML.trim().length > 0) {
|
||||
filled = true;
|
||||
setHasAdContent(true);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(el, { childList: true, subtree: true });
|
||||
|
||||
// If Ezoic doesn't fill the placeholder within 5s, hide it
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!filled && el.childNodes.length === 0) {
|
||||
setHasAdContent(false);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
observer?.disconnect();
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
runEzoic(() => {
|
||||
window.ezstandalone?.destroyPlaceholders(id);
|
||||
});
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (!hasAdContent) return null;
|
||||
|
||||
return (
|
||||
<div className="ezoic-ad-container group border rounded-[var(--radius)] overflow-hidden hover:border-foreground/20 transition-colors flex flex-col">
|
||||
<div className="px-3 py-1.5 border-b bg-muted/50">
|
||||
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{t("ad")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ezoic-ad-content overflow-hidden flex items-center justify-center">
|
||||
{isRendered && <div id={`ezoic-pub-ad-placeholder-${id}`} ref={placeholderRef} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,8 @@ interface AuthContentProps {
|
||||
providers: string[];
|
||||
mode: "login" | "register";
|
||||
useCloneBranding?: boolean;
|
||||
providerDisplayNames?: Record<string, string>;
|
||||
providerLogos?: Record<string, string>;
|
||||
}
|
||||
|
||||
const providerNames: Record<string, string> = {
|
||||
@@ -19,7 +21,7 @@ const providerNames: Record<string, string> = {
|
||||
credentials: "Email",
|
||||
};
|
||||
|
||||
export function AuthContent({ providers, mode, useCloneBranding = false }: AuthContentProps) {
|
||||
export function AuthContent({ providers, mode, useCloneBranding = false, providerDisplayNames, providerLogos }: AuthContentProps) {
|
||||
const t = useTranslations("auth");
|
||||
const hasCredentials = providers.includes("credentials");
|
||||
const oauthProviders = providers.filter((p) => p !== "credentials");
|
||||
@@ -34,7 +36,8 @@ export function AuthContent({ providers, mode, useCloneBranding = false }: AuthC
|
||||
<OAuthButton
|
||||
key={provider}
|
||||
provider={provider}
|
||||
providerName={providerNames[provider] || provider}
|
||||
providerName={providerDisplayNames?.[provider] || providerNames[provider] || provider}
|
||||
iconUrl={providerLogos?.[provider]}
|
||||
/>
|
||||
))}
|
||||
{hasGitHub && !useCloneBranding && (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { analyticsAuth } from "@/lib/analytics";
|
||||
interface OAuthButtonProps {
|
||||
provider: string;
|
||||
providerName: string;
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
const providerIcons: Record<string, React.ReactNode> = {
|
||||
@@ -38,9 +39,10 @@ const providerIcons: Record<string, React.ReactNode> = {
|
||||
),
|
||||
};
|
||||
|
||||
export function OAuthButton({ provider, providerName }: OAuthButtonProps) {
|
||||
export function OAuthButton({ provider, providerName, iconUrl }: OAuthButtonProps) {
|
||||
const t = useTranslations("auth");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
const handleSignIn = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -63,8 +65,15 @@ export function OAuthButton({ provider, providerName }: OAuthButtonProps) {
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
) : iconUrl && !imgError ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={iconUrl} alt={providerName} className="mr-2 h-4 w-4 object-contain" onError={() => setImgError(true)} />
|
||||
) : providerIcons[provider] ? (
|
||||
<span className="mr-2">{providerIcons[provider]}</span>
|
||||
) : (
|
||||
<span className="mr-2 flex h-4 w-4 items-center justify-center rounded bg-primary/10 text-[10px] font-bold text-primary">
|
||||
{providerName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
{t("signInWith", { provider: providerName })}
|
||||
</Button>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { analyticsAuth } from "@/lib/analytics";
|
||||
|
||||
const registerSchema = z.object({
|
||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||
username: z.string().min(1, "Username is required").regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores"),
|
||||
username: z.string().min(1, "Username is required").max(30, "Username must be at most 30 characters").regex(/^[a-z0-9_]+$/, "Username can only contain lowercase letters, numbers, and underscores"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters"),
|
||||
confirmPassword: z.string(),
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Sparkles, X } from "lucide-react";
|
||||
import { useBranding } from "@/components/providers/branding-provider";
|
||||
|
||||
export interface AnnouncementBannerConfig {
|
||||
// Unique id used for the localStorage dismissal key. Bump this to re-show
|
||||
// the banner after editing the message for a new announcement.
|
||||
id: string;
|
||||
// Optional href; if provided the banner becomes a link.
|
||||
href?: string;
|
||||
// Whether the link should open in a new tab. Defaults to true for external links.
|
||||
external?: boolean;
|
||||
// Main message; rendered on small+ screens.
|
||||
message: React.ReactNode;
|
||||
// Shorter message rendered on mobile (falls back to `message`).
|
||||
messageShort?: React.ReactNode;
|
||||
// Optional call-to-action label rendered after the message (e.g. "View session →").
|
||||
cta?: string;
|
||||
}
|
||||
|
||||
// Edit this to change/disable the active announcement.
|
||||
// Set to `null` to hide the banner entirely.
|
||||
export const ACTIVE_ANNOUNCEMENT: AnnouncementBannerConfig | null = {
|
||||
id: "ms-build-2026",
|
||||
href: "https://build.microsoft.com/en-US/sessions/LTG468",
|
||||
external: true,
|
||||
message: (
|
||||
<>
|
||||
<span className="font-semibold">Fatih Kadir Akın</span> is speaking about
|
||||
prompts.chat at <span className="font-semibold">Microsoft Build 2026</span>
|
||||
{" · June 2–3, San Francisco"}
|
||||
</>
|
||||
),
|
||||
messageShort: (
|
||||
<>
|
||||
<span className="font-semibold">prompts.chat</span> at Microsoft Build 2026
|
||||
</>
|
||||
),
|
||||
cta: "View session →",
|
||||
};
|
||||
|
||||
export function AnnouncementBanner({
|
||||
announcement = ACTIVE_ANNOUNCEMENT,
|
||||
}: {
|
||||
announcement?: AnnouncementBannerConfig | null;
|
||||
}) {
|
||||
const branding = useBranding();
|
||||
const searchParams = useSearchParams();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const storageKey = announcement
|
||||
? `announcement-banner-dismissed:${announcement.id}`
|
||||
: null;
|
||||
const hideViaQuery = searchParams?.has("no-announcement");
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageKey) return;
|
||||
const dismissed = localStorage.getItem(storageKey);
|
||||
if (!dismissed) setIsVisible(true);
|
||||
}, [storageKey]);
|
||||
|
||||
if (!announcement || !isVisible || branding.useCloneBranding || hideViaQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
if (storageKey) localStorage.setItem(storageKey, "true");
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Sparkles className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||||
<span className="truncate">
|
||||
<span className="hidden sm:inline">{announcement.message}</span>
|
||||
<span className="sm:hidden">
|
||||
{announcement.messageShort ?? announcement.message}
|
||||
</span>
|
||||
{announcement.cta && (
|
||||
<span className="ml-1 underline-offset-2 hover:underline">
|
||||
{announcement.cta}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-r from-primary/90 to-primary text-primary-foreground">
|
||||
<div className="container flex items-center justify-between gap-3 py-1.5 text-xs sm:text-sm">
|
||||
{announcement.href ? (
|
||||
<Link
|
||||
href={announcement.href}
|
||||
target={announcement.external ? "_blank" : undefined}
|
||||
rel={announcement.external ? "noopener noreferrer" : undefined}
|
||||
className="flex min-w-0 flex-1 items-center gap-2 hover:underline"
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">{content}</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
className="-mr-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-primary-foreground/80 hover:bg-primary-foreground/20 hover:text-primary-foreground"
|
||||
aria-label="Dismiss announcement"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||