diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dee7e8c --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +DATABASE_URL="postgresql://saas:saas@localhost:5432/saas?schema=public" +EMAIL_FROM="Saas app " +SMTP_HOST=localhost +SMTP_PORT=25 +SMTP_USER= +SMTP_PASSWORD= +NODE_TLS_REJECT_UNAUTHORIZED="0" +NEXTAUTH_URL="http://localhost:3000" +NEXTAUTH_SECRET=RANDOMEKEY +NEXT_PUBLIC_SLACK_CLIENT_ID= +SLACK_CLIENT_SECRET= +NEXT_PUBLIC_SENTRY_DSN=https://xxx@oxxx.ingest.sentry.io/xxx +TYPEFULLY_API_URL="https://api.typefully.com/v1" +JWT_SECRET="secret" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..1c2aa65 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5130fef --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: 'run tests' +on: + push: + branches: + - web-v2 + - main + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: 'Create env file' + run: | + touch .env + echo NEXTAUTH_SECRET="RANDOMEKEY" >> .env + echo JWT_SECRET="RANDOMEKEY" >> .env + cat .env + - name: Install dependencies + run: yarn install + - name: Run tests + run: yarn test:build + - name: Run build + run: yarn build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad4c378 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.mailing + +# Sentry +.sentryclirc + +# Sentry +next.config.original.js + +# Sentry Auth Token +.sentryclirc + +#Content Layer +.contentlayer diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8981b68 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "editorconfig": false, + "singleQuote": true, + "jsxSingleQuote": false, + "tabWidth": 2, + "semi": true, + "trailingComma": "es5", + "printWidth": 80, + "bracketSpacing": true, + "arrowParens": "always", + "quoteProps": "consistent" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cbe69d2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + + "prettier.configPath": ".prettierrc", + "prettier.useEditorConfig": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..ac1cca9 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +docker-compose = "*" + +[dev-packages] + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..f5cf448 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,462 @@ +{ + "_meta": { + "hash": { + "sha256": "7ec163365261d72ea1584003cb5310b56cb8422a1eb293d969a246538cca0d05" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", + "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" + ], + "markers": "python_version >= '3.6'", + "version": "==22.2.0" + }, + "bcrypt": { + "hashes": [ + "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", + "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", + "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", + "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", + "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", + "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", + "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", + "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", + "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", + "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", + "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", + "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", + "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", + "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", + "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", + "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", + "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", + "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", + "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", + "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", + "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "certifi": { + "hashes": [ + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + ], + "markers": "python_version >= '3.6'", + "version": "==2022.12.7" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", + "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", + "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", + "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", + "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", + "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", + "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", + "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", + "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", + "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", + "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", + "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", + "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", + "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", + "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", + "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", + "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", + "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", + "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", + "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", + "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", + "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", + "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", + "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", + "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", + "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", + "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", + "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", + "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", + "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", + "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", + "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", + "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", + "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", + "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", + "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", + "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", + "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", + "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", + "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", + "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", + "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", + "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", + "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", + "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", + "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", + "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", + "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", + "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", + "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", + "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", + "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", + "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", + "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", + "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", + "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", + "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", + "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", + "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", + "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", + "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", + "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", + "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", + "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", + "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", + "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", + "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", + "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", + "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", + "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", + "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", + "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", + "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", + "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", + "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", + "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", + "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", + "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", + "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", + "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", + "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", + "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", + "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", + "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", + "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", + "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", + "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", + "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" + ], + "version": "==3.0.1" + }, + "cryptography": { + "hashes": [ + "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b", + "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f", + "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190", + "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f", + "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f", + "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb", + "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c", + "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773", + "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72", + "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8", + "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717", + "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9", + "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856", + "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96", + "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288", + "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39", + "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e", + "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce", + "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1", + "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de", + "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df", + "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf", + "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458" + ], + "markers": "python_version >= '3.6'", + "version": "==39.0.0" + }, + "distro": { + "hashes": [ + "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8", + "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff" + ], + "markers": "python_version >= '3.6'", + "version": "==1.8.0" + }, + "docker": { + "extras": [ + "ssh" + ], + "hashes": [ + "sha256:896c4282e5c7af5c45e8b683b0b0c33932974fe6e50fc6906a0a83616ab3da97", + "sha256:dbcb3bd2fa80dca0788ed908218bf43972772009b881ed1e20dfc29a65e49782" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.1" + }, + "docker-compose": { + "hashes": [ + "sha256:4c8cd9d21d237412793d18bd33110049ee9af8dab3fe2c213bbd0733959b09b7", + "sha256:8d5589373b35c8d3b1c8c1182c6e4a4ff14bffa3dd0b605fcd08f73c94cef809" + ], + "index": "pypi", + "version": "==1.29.2" + }, + "dockerpty": { + "hashes": [ + "sha256:69a9d69d573a0daa31bcd1c0774eeed5c15c295fe719c61aca550ed1393156ce" + ], + "version": "==0.4.1" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "jsonschema": { + "hashes": [ + "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", + "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" + ], + "version": "==3.2.0" + }, + "packaging": { + "hashes": [ + "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", + "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0" + }, + "paramiko": { + "hashes": [ + "sha256:6bef55b882c9d130f8015b9a26f4bd93f710e90fe7478b9dcc810304e79b3cd8", + "sha256:fedc9b1dd43bc1d45f67f1ceca10bc336605427a46dcdf8dec6bfea3edf57965" + ], + "version": "==3.0.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pynacl": { + "hashes": [ + "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", + "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", + "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", + "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", + "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", + "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", + "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", + "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", + "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", + "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" + ], + "markers": "python_version >= '3.6'", + "version": "==1.5.0" + }, + "pyrsistent": { + "hashes": [ + "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", + "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", + "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", + "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", + "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", + "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", + "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", + "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", + "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", + "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", + "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", + "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", + "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", + "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", + "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", + "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", + "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", + "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", + "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", + "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", + "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", + "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", + "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", + "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", + "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", + "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", + "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" + ], + "markers": "python_version >= '3.7'", + "version": "==0.19.3" + }, + "python-dotenv": { + "hashes": [ + "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49", + "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a" + ], + "markers": "python_version >= '3.7'", + "version": "==0.21.1" + }, + "pyyaml": { + "hashes": [ + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==5.4.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "texttable": { + "hashes": [ + "sha256:290348fb67f7746931bcdfd55ac7584ecd4e5b0846ab164333f0794b121760f2", + "sha256:b7b68139aa8a6339d2c320ca8b1dc42d13a7831a346b446cb9eb385f0c76310c" + ], + "version": "==1.6.7" + }, + "urllib3": { + "hashes": [ + "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", + "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.14" + }, + "websocket-client": { + "hashes": [ + "sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32", + "sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.59.0" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..22850b0 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +
+ + Digest.club: The frontpage of teams knowledge + +

The frontpage of teams knowledge

+
+ +Unlock the power of streamlined knowledge management with Digest Club, the solution for teams looking to centralize their information gathering, sharing, and curation. Say goodbye to scattered bookmarks, disjointed Slack conversations, and missed opportunities to showcase your expertise. + +👉 [https://digest.club](https://digest.club) + +## Features + +- 📚 Bookmarking: Easily add and organize links via our website. +- 🔗 Slack Integration: Automatically import links posted in Slack channels. +- 📅 Periodic Digests: Create curated digests at scheduled intervals. +- 💌 Newsletter Sharing: Share digests in a polished newsletter format. +- 🐦 Twitter Threads: Transform digests into engaging Twitter threads. +- ✨ Markdown Enrichment: Enhance digests with markdown-enhanced content. + +## Stack + +- ▲ [Next.js](https://nextjs.org/) for webapp (app dir + TS) +- 🖼 [Tailwind](https://tailwindcss.com/) for UI components +- 📦 [Prisma](https://www.prisma.io/) for database + +## Getting Started + +Install dependencies: + +```bash +pipenv install +pipenv shell +yarn install +``` + +You can use Docker to run a local postgres database and maildev server (accessible at http://localhost:1080): + +```bash +docker-compose up -d +``` + +Create .env: + +```bash +cp .env.example .env +``` + +Run migrations + +```bash +yarn prisma:migrate:dev +``` + +Run the development server: + +```bash +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/changelogs/changelog-001.md b/changelogs/changelog-001.md new file mode 100644 index 0000000..0b09729 --- /dev/null +++ b/changelogs/changelog-001.md @@ -0,0 +1,28 @@ +--- +title: 🎉 Introducing Typefully Integration +publishedAt: 2023-05-26 +image: changelog-001.webp +--- + +### New Features + +- **Typefully Integration**: We are excited to announce our latest feature - _Typefully_ integration! With just a single click, you can now transform your Digest into a Twitter thread (draft) using the [Typefully](https://typefully.com/) service. Simply provide us with your _Typefully_ API token, and effortlessly share your curated links with your audience on Twitter. + +- **Deleting members of a Team**: Team's administrators can now delete members, by clicking on the _Delete_ button next to the member's name. + +### Enhancements + +- **Improved User Interface**: We have refined the user interface with an updated navigation +- **'Updates' page**: We have added a [new page](https://digest.club/updates) to keep you up-to-date with the latest changes and improvements to Digest.club. + +### Get Started with Typefully Integration + +To take advantage of the Typefully integration, follow these simple steps: + +1. Sign up for a Typefully account at [typefully.com](https://typefully.com) and obtain your API token. +2. Navigate to the 'Settings' page of your team and enter your Typefully API token in the designated field. +3. Once your API token is saved, you will find a new 'Create Twitter Thread' option when viewing your Digest. Click on it to effortlessly transform your Digest into a draft Twitter thread. + +We hope you enjoy this new features and find it valuable for sharing your technological discoveries with the world! + +Thank you for choosing Digest.club, and happy digesting! 🚀 diff --git a/changelogs/changelog-002.md b/changelogs/changelog-002.md new file mode 100644 index 0000000..bee4732 --- /dev/null +++ b/changelogs/changelog-002.md @@ -0,0 +1,41 @@ +--- +title: 📰 Newsletter +publishedAt: 2023-06-02 +image: changelog-002.webp +--- + +### New Features + +- **Newsletter**: We are excited to introduce the Newsletter feature! Teams can now send their Digests to all their subscribers in the form of a newsletter. Users can now stay connected with the latest curated links and updates from their favorite teams right in their inbox. + +### Enhancements + +- **Subscription Form**: We have added a subscription form to the digest's public page and the team's public page. Visitors can now easily subscribe to receive newsletters from their preferred teams. +- **Subscriber Management**: Team administrators can see a list of their subscribers in the team's settings page, under the _Newsletter_ tab. + +### Getting Started with Newsletters + +To start receiving newsletters, follow these simple steps: + +1. Visit the public digest page or the public digests page of the team you wish to subscribe to. +2. Fill out the subscription form with your email address and click **Subscribe**. +3. Sit back and enjoy receiving curated Digests from your subscribed teams directly in your inbox! + +### Sending Newsletters + +Team members can send newsletters to their subscribers through the following steps: + +1. Go to the Digest builder page. +2. Click on the **Send Newsletter** button. +3. Confirm your choice to send the newsletter, and you are done ! _You can only send a newsletter once._ + +### Unsubscribing from Newsletters + +To unsubscribe from a team's newsletter, follow these steps: + +1. Open any email received from the team's newsletter. +2. Scroll to the bottom of the email and click the **unsubscribe** link. + +We hope you find the Newsletter feature helpful in staying up-to-date with your favorite team's digests. Happy reading! + +Thank you for choosing Digest.club, and happy digesting! 🚀 diff --git a/changelogs/changelog-003.md b/changelogs/changelog-003.md new file mode 100644 index 0000000..ecc9b91 --- /dev/null +++ b/changelogs/changelog-003.md @@ -0,0 +1,43 @@ +--- +title: 🖊️ Introducing Text block +publishedAt: 2023-06-07 +image: changelog-003.webp +--- + +### New Features + +- **Text Block**: We are thrilled to introduce the text block feature! You can now add text blocks that support **markdown** formatting to your Digests. Customize your content with headings, lists, links, emphasis, and more to create rich and engaging Digests. + +### Getting Started with Markdown Blocks + +To start using the Markdown Block feature, follow these simple steps: + +1. Open the Digest builder for the Digest you wish to edit or create a new Digest. +2. Click on the ellipsis (`...`) button in the top right corner of a block to open the block menu. +3. Then, click on the **Add text block after** button. +4. Start writing or paste your markdown content into the block editor. +5. Save your changes, and the markdown block will be added to your Digest. + +### Markdown Syntax Guide + +Use familiar markdown syntax to format your text within the new text block: + +- Headings: Use hashtags (`#`) for different heading levels. +- Lists: Create ordered lists using numbers or unordered lists using dashes (`-`) or asterisks (`*`). +- Links: Enclose the link text in square brackets (`[]`) followed by the URL in parentheses (`()`). +- Emphasis: Add emphasis to text using asterisks (`*`) or underscores (`_`). +- Code: Wrap code snippets with backticks (\```) to preserve their formatting. +- And more: Explore additional markdown syntax options for formatting your content. + +### Tips for effective use of text blocks + +Here are a few tips to make the most out of text blocks in your Digests: + +1. Use headings to create clear sections and structure within your Digest. +2. Format important information or highlights using emphasis or bullet points. +3. Incorporate links to external resources or relevant articles to provide additional context. +4. Experiment with different markdown features to make your Digests visually appealing and easy to read. + +Please keep in mind that Markdown blocks cannot be converted to tweet. As a result, they will not be compatible with thread generation via _Typefully_ + +Thank you for choosing Digest.club, and happy digesting! 🚀 diff --git a/changelogs/changelog-004.md b/changelogs/changelog-004.md new file mode 100644 index 0000000..71e985d --- /dev/null +++ b/changelogs/changelog-004.md @@ -0,0 +1,49 @@ +--- +title: 🚀 Introducing API for bookmarking +publishedAt: 2023-06-15 +image: changelog-004.webp +--- + +### New Features + +- **API for Bookmarking**: We are happy to announce the introduction of our first API endpoint dedicated to adding bookmarks. Now, you can seamlessly integrate bookmarking functionality into your workflows using our API with your **API key**. + +### Getting your Team's API Key + +To generate an **API key**, follow these simple steps: + +1. Go to the "Settings" page of your team in [digest.club](https://digest.club/). +2. Click on "Create New" to generate a new API key. +3. The API key will be generated, and you can copy it to your clipboard by clicking on the icon in the input. + +### Adding a Bookmark via API + +To add a bookmark via the API, make an HTTP **POST** request to this endpoint: _/api/bookmark_. + +Include the bookmark URL in the request body under the **linkUrl** field. Additionally, to authenticate yourself with the API, pass the API key in the _"Authorization"_ header with the _"Bearer "_ prefix. + +``` +Method: POST +Endpoint: https://www.digest.club/api/bookmark +Headers: +- Authorization: Bearer YOUR_API_KEY + +Body: + { + "linkUrl": "https://www.example.com" + } +``` + +Here's an example using cURL: + +```bash +curl -X POST https://www.digest.club/api/bookmark \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -d "linkUrl=https://www.example.com" +``` + +## Integration Possibilities + +The API for Bookmarking opens up possibilities for integrating Digest.club into your workflows. Whether you want to automate bookmarking or sync Digests with other platforms, the API provides the flexibility you need. + +Happy digesting! 🚀 diff --git a/changelogs/changelog-005.md b/changelogs/changelog-005.md new file mode 100644 index 0000000..a2cef7f --- /dev/null +++ b/changelogs/changelog-005.md @@ -0,0 +1,21 @@ +--- +title: 🐦 Tweet / X Embeds +publishedAt: 2023-08-21 +image: changelog-005.webp +--- + +### New Feature + +- **Tweet / X Embeds**: We are introducing the ability to display tweets in an "embed" format within a Digest! When editing a Digest, click on the ellipsis icon of the tweet's block and select the "Tweet" option from the block style. This feature is only available for tweet bookmark. + +### Enhancements + +- **Default Image for URLs**: Bookmarks without open-graph images will now display a default image with the site's title and favicon. This enhancement ensures a visually consistent experience for your content. + +- **Performance Improvements**: We've implemented several performance enhancements on the page displaying a team's Digests, ensuring faster load times. + +### Bug Fixes + +- **Twitter Link Import Bug Fix**: We've resolved the bug that was previously blocking the import of Twitter links. + +- **Various Bug Fixes**: We've addressed various bugs and issues throughout the application, ensuring a more stable and reliable experience. diff --git a/contentlayer.config.ts b/contentlayer.config.ts new file mode 100644 index 0000000..20e19e2 --- /dev/null +++ b/contentlayer.config.ts @@ -0,0 +1,20 @@ +// contentlayer.config.ts +import { defineDocumentType, makeSource } from '@contentlayer/source-files'; + +export const Changelog = defineDocumentType(() => ({ + name: 'Changelog', + filePathPattern: `**/*.md`, + fields: { + title: { type: 'string', required: true }, + publishedAt: { type: 'date', required: true }, + image: { + type: 'string', + required: false, + }, + }, +})); + +export default makeSource({ + contentDirPath: 'changelogs', + documentTypes: [Changelog], +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..272204d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3' + +services: + postgres: + image: postgres:14 + restart: always + environment: + POSTGRES_USER: saas + POSTGRES_PASSWORD: saas + POSTGRES_DB: saas + ports: + - 5432:5432 + volumes: + - postgresql:/var/lib/postgresql + - postgresql_data:/var/lib/postgresql/data + + maildev: + image: djfarrelly/maildev + ports: + - '1080:80' + - '25:25' +volumes: + postgresql: + postgresql_data: diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..2a34048 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,15 @@ +import type { Config } from '@jest/types'; +const nextJest = require('next/jest'); + +const createJestConfig = nextJest({ + dir: './', +}); +const customJestConfig: Config.InitialOptions = { + verbose: true, + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + moduleDirectories: ['node_modules', 'src/'], + testEnvironment: 'jest-environment-jsdom', +}; +module.exports = createJestConfig(customJestConfig); diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..662b9bc --- /dev/null +++ b/next.config.js @@ -0,0 +1,53 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ +const { withSentryConfig } = require('@sentry/nextjs'); +const { withContentlayer } = require('next-contentlayer') + + +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + appDir: true, + serverComponentsExternalPackages: ['mjml', 'mjml-react'], + }, + reactStrictMode: false, +}; + +module.exports = nextConfig; + + +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}) + +module.exports = withBundleAnalyzer(withContentlayer(withSentryConfig( + module.exports, + { + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options + + // Suppresses source map uploading logs during build + silent: true, + + org: 'premier-octet-z6', + project: 'digestclub', + }, + { + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Transpiles SDK to be compatible with IE11 (increases bundle size) + transpileClientSDK: true, + + // Hides source maps from generated client bundles + hideSourceMaps: true, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + } +))); diff --git a/package.json b/package.json new file mode 100644 index 0000000..9d41671 --- /dev/null +++ b/package.json @@ -0,0 +1,106 @@ +{ + "name": "saas", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "postinstall": "prisma generate", + "prisma:migrate:dev": "prisma migrate dev", + "vercel-build": "prisma generate && prisma migrate deploy && next build", + "test:watch": "jest --watch", + "test:build": "jest", + "analyze": "cross-env ANALYZE=true next build" + }, + "dependencies": { + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@heroicons/react": "^2.0.16", + "@next-auth/prisma-adapter": "^1.0.5", + "@premieroctet/next-admin": "1.3.8", + "@prisma/client": "^4.11.0", + "@radix-ui/react-dialog": "^1.0.3", + "@radix-ui/react-navigation-menu": "^1.1.2", + "@radix-ui/react-popover": "1.0.5", + "@radix-ui/react-tabs": "^1.0.3", + "@radix-ui/react-tooltip": "^1.0.5", + "@react-icons/all-files": "^4.1.0", + "@resvg/resvg-js": "^2.4.1", + "@sendinblue/client": "^3.3.1", + "@sentry/nextjs": "^7.51.2", + "@sentry/node": "^7.51.2", + "@tailwindcss/line-clamp": "^0.4.4", + "@tremor/react": "^3.4.1", + "@types/jsonwebtoken": "^9.0.2", + "@vercel/og": "^0.4.0", + "axios": "^1.3.4", + "class-variance-authority": "^0.4.0", + "contentlayer": "^0.3.2", + "date-fns": "^2.29.3", + "feed": "^4.2.2", + "framer-motion": "^10.7.0", + "jsonwebtoken": "^9.0.1", + "lodash": "^4.17.21", + "metascraper": "^5.36.0", + "metascraper-description": "^5.33.7", + "metascraper-image": "^5.33.7", + "metascraper-logo": "^5.36.0", + "metascraper-logo-favicon": "^5.36.0", + "metascraper-media-provider": "^5.33.9", + "metascraper-title": "^5.33.7", + "metascraper-twitter": "^5.33.7", + "mjml": "^4.13.0", + "mjml-react": "^2.0.8", + "next": "^13.3.0", + "next-auth": "^4.20.1", + "next-connect": "^1.0.0-next.3", + "next-contentlayer": "^0.3.2", + "next-superjson-plugin": "^0.5.8", + "nodemailer": "^6.9.1", + "plaiceholder": "^2.5.0", + "prisma": "^4.11.0", + "prisma-json-schema-generator": "^3.1.3", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.43.7", + "react-hot-toast": "^2.4.0", + "react-query": "^3.39.3", + "react-tweet": "^3.1.1", + "remark": "^14.0.3", + "remark-html": "^15.0.2", + "satori": "^0.10.2", + "sharp": "^0.31.3", + "superjson": "^1.12.2", + "url-slug": "^3.0.4" + }, + "devDependencies": { + "@next/bundle-analyzer": "^13.4.16", + "@tailwindcss/forms": "^0.5.3", + "@tailwindcss/typography": "^0.5.9", + "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.194", + "@types/mjml": "^4.7.0", + "@types/mjml-react": "^2.0.6", + "@types/node": "^18.14.5", + "@types/nodemailer": "^6.4.7", + "@types/react": "18.0.28", + "@types/react-beautiful-dnd": "^13.1.3", + "@types/react-dom": "18.0.11", + "autoprefixer": "^10.4.14", + "clsx": "^1.2.1", + "cross-env": "^7.0.3", + "eslint": "8.35.0", + "eslint-config-next": "^13.3.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "postcss": "^8.4.21", + "tailwindcss": "^3.2.7", + "tailwindcss-animate": "^1.0.5", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/prisma/json-schema/json-schema.json b/prisma/json-schema/json-schema.json new file mode 100644 index 0000000..224a9d4 --- /dev/null +++ b/prisma/json-schema/json-schema.json @@ -0,0 +1,636 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Account": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "providerAccountId": { + "type": "string" + }, + "refresh_token": { + "type": [ + "string", + "null" + ] + }, + "access_token": { + "type": [ + "string", + "null" + ] + }, + "expires_at": { + "type": [ + "integer", + "null" + ] + }, + "token_type": { + "type": [ + "string", + "null" + ] + }, + "scope": { + "type": [ + "string", + "null" + ] + }, + "id_token": { + "type": [ + "string", + "null" + ] + }, + "session_state": { + "type": [ + "string", + "null" + ] + }, + "user": { + "$ref": "#/definitions/User" + } + } + }, + "Session": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionToken": { + "type": "string" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "user": { + "$ref": "#/definitions/User" + } + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "email": { + "type": [ + "string", + "null" + ] + }, + "emailVerified": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/Account" + } + }, + "sessions": { + "type": "array", + "items": { + "$ref": "#/definitions/Session" + } + }, + "role": { + "type": "string", + "default": "USER", + "enum": [ + "SUPERADMIN", + "USER" + ] + }, + "memberships": { + "type": "array", + "items": { + "$ref": "#/definitions/Membership" + } + }, + "defaultTeam": { + "anyOf": [ + { + "$ref": "#/definitions/Team" + }, + { + "type": "null" + } + ] + } + } + }, + "VerificationToken": { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "token": { + "type": "string" + }, + "expires": { + "type": "string", + "format": "date-time" + } + } + }, + "Team": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "memberships": { + "type": "array", + "items": { + "$ref": "#/definitions/Membership" + } + }, + "subscriptions": { + "type": "array", + "items": { + "$ref": "#/definitions/Subscription" + } + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + }, + "Digest": { + "type": "array", + "items": { + "$ref": "#/definitions/Digest" + } + }, + "bookmarks": { + "type": "array", + "items": { + "$ref": "#/definitions/Bookmark" + } + }, + "slackToken": { + "type": [ + "string", + "null" + ] + }, + "slackTeamId": { + "type": [ + "string", + "null" + ] + }, + "typefullyToken": { + "type": [ + "string", + "null" + ] + }, + "bio": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + }, + "twitter": { + "type": [ + "string", + "null" + ] + }, + "github": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "apiKey": { + "type": [ + "string", + "null" + ] + } + } + }, + "Membership": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "ADMIN", + "USER" + ] + }, + "team": { + "$ref": "#/definitions/Team" + }, + "user": { + "anyOf": [ + { + "$ref": "#/definitions/User" + }, + { + "type": "null" + } + ] + }, + "invitedName": { + "type": [ + "string", + "null" + ] + }, + "invitedEmail": { + "type": [ + "string", + "null" + ] + }, + "invitations": { + "type": "array", + "items": { + "$ref": "#/definitions/Invitation" + } + }, + "bookmark": { + "type": "array", + "items": { + "$ref": "#/definitions/Bookmark" + } + } + } + }, + "Invitation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "membership": { + "$ref": "#/definitions/Membership" + }, + "expiredAt": { + "type": "string", + "format": "date-time" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "validatedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } + }, + "Link": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "blurHash": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "logo": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "bookmark": { + "type": "array", + "items": { + "$ref": "#/definitions/Bookmark" + } + } + } + }, + "Bookmark": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "link": { + "$ref": "#/definitions/Link" + }, + "membership": { + "anyOf": [ + { + "$ref": "#/definitions/Membership" + }, + { + "type": "null" + } + ] + }, + "team": { + "$ref": "#/definitions/Team" + }, + "provider": { + "type": "string", + "default": "WEB", + "enum": [ + "WEB", + "SLACK" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "digestBlocks": { + "type": "array", + "items": { + "$ref": "#/definitions/DigestBlock" + } + }, + "metadata": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + } + } + }, + "Digest": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "publishedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "team": { + "$ref": "#/definitions/Team" + }, + "digestBlocks": { + "type": "array", + "items": { + "$ref": "#/definitions/DigestBlock" + } + }, + "typefullyThreadUrl": { + "type": [ + "string", + "null" + ] + }, + "hasSentNewsletter": { + "type": "boolean", + "default": false + } + } + }, + "DigestBlock": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "bookmark": { + "anyOf": [ + { + "$ref": "#/definitions/Bookmark" + }, + { + "type": "null" + } + ] + }, + "digest": { + "$ref": "#/definitions/Digest" + }, + "order": { + "type": "integer" + }, + "style": { + "type": "string", + "default": "BLOCK", + "enum": [ + "BLOCK", + "INLINE", + "TWEET_EMBED" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "default": "BOOKMARK", + "enum": [ + "BOOKMARK", + "TEXT" + ] + }, + "text": { + "type": [ + "string", + "null" + ] + } + } + }, + "Subscription": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "team": { + "$ref": "#/definitions/Team" + }, + "email": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + } + }, + "type": "object", + "properties": { + "account": { + "$ref": "#/definitions/Account" + }, + "session": { + "$ref": "#/definitions/Session" + }, + "user": { + "$ref": "#/definitions/User" + }, + "verificationToken": { + "$ref": "#/definitions/VerificationToken" + }, + "team": { + "$ref": "#/definitions/Team" + }, + "membership": { + "$ref": "#/definitions/Membership" + }, + "invitation": { + "$ref": "#/definitions/Invitation" + }, + "link": { + "$ref": "#/definitions/Link" + }, + "bookmark": { + "$ref": "#/definitions/Bookmark" + }, + "digest": { + "$ref": "#/definitions/Digest" + }, + "digestBlock": { + "$ref": "#/definitions/DigestBlock" + }, + "subscription": { + "$ref": "#/definitions/Subscription" + } + } +} \ No newline at end of file diff --git a/prisma/migrations/20230107175741_init/migration.sql b/prisma/migrations/20230107175741_init/migration.sql new file mode 100644 index 0000000..b71c956 --- /dev/null +++ b/prisma/migrations/20230107175741_init/migration.sql @@ -0,0 +1,121 @@ +-- CreateEnum +CREATE TYPE "MembershipRole" AS ENUM ('ADMIN', 'USER'); + +-- CreateEnum +CREATE TYPE "GlobalRole" AS ENUM ('SUPERADMIN', 'USER'); + +-- CreateTable +CREATE TABLE "accounts" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "accounts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sessions" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "name" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "role" "GlobalRole" NOT NULL DEFAULT 'USER', + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "verification_tokens" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "teams" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + + CONSTRAINT "teams_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "memberships" ( + "id" SERIAL NOT NULL, + "role" "MembershipRole" NOT NULL, + "teamId" INTEGER NOT NULL, + "userId" TEXT, + "invitedName" TEXT, + "invitedEmail" TEXT, + + CONSTRAINT "memberships_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "invitations" ( + "id" TEXT NOT NULL, + "membershipId" INTEGER NOT NULL, + "expiredAt" TIMESTAMP(3) NOT NULL, + "validatedAt" TIMESTAMP(3), + + CONSTRAINT "invitations_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "accounts_provider_providerAccountId_key" ON "accounts"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "sessions_sessionToken_key" ON "sessions"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "verification_tokens_token_key" ON "verification_tokens"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "verification_tokens_identifier_token_key" ON "verification_tokens"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "teams_slug_key" ON "teams"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "memberships_teamId_invitedEmail_key" ON "memberships"("teamId", "invitedEmail"); + +-- AddForeignKey +ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "memberships" ADD CONSTRAINT "memberships_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "memberships" ADD CONSTRAINT "memberships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "invitations" ADD CONSTRAINT "invitations_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "memberships"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230130170117_add_created_date_to_invitations/migration.sql b/prisma/migrations/20230130170117_add_created_date_to_invitations/migration.sql new file mode 100644 index 0000000..a89ce93 --- /dev/null +++ b/prisma/migrations/20230130170117_add_created_date_to_invitations/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "invitations" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/migrations/20230210104913_add_default_team/migration.sql b/prisma/migrations/20230210104913_add_default_team/migration.sql new file mode 100644 index 0000000..4894906 --- /dev/null +++ b/prisma/migrations/20230210104913_add_default_team/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "defaultTeamId" INTEGER; + +-- AddForeignKey +ALTER TABLE "users" ADD CONSTRAINT "users_defaultTeamId_fkey" FOREIGN KEY ("defaultTeamId") REFERENCES "teams"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20230213142412_create_links_and_bookmarks/migration.sql b/prisma/migrations/20230213142412_create_links_and_bookmarks/migration.sql new file mode 100644 index 0000000..f939ec2 --- /dev/null +++ b/prisma/migrations/20230213142412_create_links_and_bookmarks/migration.sql @@ -0,0 +1,34 @@ +-- CreateEnum +CREATE TYPE "Provider" AS ENUM ('WEB'); + +-- CreateTable +CREATE TABLE "links" ( + "id" TEXT NOT NULL, + "url" TEXT NOT NULL, + "image" TEXT, + "blurHash" TEXT, + "title" TEXT, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "links_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "bookmarks" ( + "id" TEXT NOT NULL, + "linkId" TEXT NOT NULL, + "membershipId" INTEGER NOT NULL, + "provider" "Provider" NOT NULL DEFAULT 'WEB', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "bookmarks_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_linkId_fkey" FOREIGN KEY ("linkId") REFERENCES "links"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "memberships"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230307140817_add_digest_model_squashed_migrations/migration.sql b/prisma/migrations/20230307140817_add_digest_model_squashed_migrations/migration.sql new file mode 100644 index 0000000..1ffa286 --- /dev/null +++ b/prisma/migrations/20230307140817_add_digest_model_squashed_migrations/migration.sql @@ -0,0 +1,24 @@ +-- AlterTable +ALTER TABLE "bookmarks" ADD COLUMN "digestId" TEXT; + +-- CreateTable +CREATE TABLE "digests" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "publishedAt" TIMESTAMP(3), + "teamId" INTEGER NOT NULL, + + CONSTRAINT "digests_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "digests_teamId_title_key" ON "digests"("teamId", "title"); + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_digestId_fkey" FOREIGN KEY ("digestId") REFERENCES "digests"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "digests" ADD CONSTRAINT "digests_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230315093524_remove_unique_constraint_title/migration.sql b/prisma/migrations/20230315093524_remove_unique_constraint_title/migration.sql new file mode 100644 index 0000000..b8a084d --- /dev/null +++ b/prisma/migrations/20230315093524_remove_unique_constraint_title/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "digests_teamId_title_key"; diff --git a/prisma/migrations/20230315134428_add_bookmark_digest/migration.sql b/prisma/migrations/20230315134428_add_bookmark_digest/migration.sql new file mode 100644 index 0000000..6e547fe --- /dev/null +++ b/prisma/migrations/20230315134428_add_bookmark_digest/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - You are about to drop the column `digestId` on the `bookmarks` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "bookmarks" DROP CONSTRAINT "bookmarks_digestId_fkey"; + +-- AlterTable +ALTER TABLE "bookmarks" DROP COLUMN "digestId"; + +-- CreateTable +CREATE TABLE "bookmark_digest" ( + "id" TEXT NOT NULL, + "bookmarkId" TEXT NOT NULL, + "digestId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + + CONSTRAINT "bookmark_digest_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "bookmark_digest_bookmarkId_digestId_key" ON "bookmark_digest"("bookmarkId", "digestId"); + +-- AddForeignKey +ALTER TABLE "bookmark_digest" ADD CONSTRAINT "bookmark_digest_bookmarkId_fkey" FOREIGN KEY ("bookmarkId") REFERENCES "bookmarks"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmark_digest" ADD CONSTRAINT "bookmark_digest_digestId_fkey" FOREIGN KEY ("digestId") REFERENCES "digests"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230315170514_add_slack_token/migration.sql b/prisma/migrations/20230315170514_add_slack_token/migration.sql new file mode 100644 index 0000000..1762137 --- /dev/null +++ b/prisma/migrations/20230315170514_add_slack_token/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "teams" ADD COLUMN "slackTeamId" TEXT, +ADD COLUMN "slackToken" TEXT; diff --git a/prisma/migrations/20230315222747_add_team/migration.sql b/prisma/migrations/20230315222747_add_team/migration.sql new file mode 100644 index 0000000..b6d0982 --- /dev/null +++ b/prisma/migrations/20230315222747_add_team/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - Added the required column `teamId` to the `bookmarks` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "bookmarks" DROP CONSTRAINT "bookmarks_membershipId_fkey"; + +-- AlterTable +ALTER TABLE "bookmarks" ADD COLUMN "teamId" INTEGER NOT NULL, +ALTER COLUMN "membershipId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "memberships"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230315223842_update_enum/migration.sql b/prisma/migrations/20230315223842_update_enum/migration.sql new file mode 100644 index 0000000..aa4bca5 --- /dev/null +++ b/prisma/migrations/20230315223842_update_enum/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Provider" ADD VALUE 'SLACK'; diff --git a/prisma/migrations/20230316093529_add_metadata_bookmark/migration.sql b/prisma/migrations/20230316093529_add_metadata_bookmark/migration.sql new file mode 100644 index 0000000..636a333 --- /dev/null +++ b/prisma/migrations/20230316093529_add_metadata_bookmark/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "bookmarks" ADD COLUMN "metadata" JSONB; diff --git a/prisma/migrations/20230316100407_add_unique_constraint_team_id/migration.sql b/prisma/migrations/20230316100407_add_unique_constraint_team_id/migration.sql new file mode 100644 index 0000000..d783623 --- /dev/null +++ b/prisma/migrations/20230316100407_add_unique_constraint_team_id/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[id,slackTeamId]` on the table `teams` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "teams_id_slackTeamId_key" ON "teams"("id", "slackTeamId"); diff --git a/prisma/migrations/20230316154959_add_delete_cascade_bookmark/migration.sql b/prisma/migrations/20230316154959_add_delete_cascade_bookmark/migration.sql new file mode 100644 index 0000000..2a13678 --- /dev/null +++ b/prisma/migrations/20230316154959_add_delete_cascade_bookmark/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "bookmark_digest" DROP CONSTRAINT "bookmark_digest_bookmarkId_fkey"; + +-- DropForeignKey +ALTER TABLE "bookmark_digest" DROP CONSTRAINT "bookmark_digest_digestId_fkey"; + +-- AddForeignKey +ALTER TABLE "bookmark_digest" ADD CONSTRAINT "bookmark_digest_bookmarkId_fkey" FOREIGN KEY ("bookmarkId") REFERENCES "bookmarks"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmark_digest" ADD CONSTRAINT "bookmark_digest_digestId_fkey" FOREIGN KEY ("digestId") REFERENCES "digests"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20230316171352_add_cascade_behaviours/migration.sql b/prisma/migrations/20230316171352_add_cascade_behaviours/migration.sql new file mode 100644 index 0000000..d01b40d --- /dev/null +++ b/prisma/migrations/20230316171352_add_cascade_behaviours/migration.sql @@ -0,0 +1,35 @@ +-- DropForeignKey +ALTER TABLE "bookmarks" DROP CONSTRAINT "bookmarks_linkId_fkey"; + +-- DropForeignKey +ALTER TABLE "bookmarks" DROP CONSTRAINT "bookmarks_teamId_fkey"; + +-- DropForeignKey +ALTER TABLE "digests" DROP CONSTRAINT "digests_teamId_fkey"; + +-- DropForeignKey +ALTER TABLE "invitations" DROP CONSTRAINT "invitations_membershipId_fkey"; + +-- DropForeignKey +ALTER TABLE "memberships" DROP CONSTRAINT "memberships_teamId_fkey"; + +-- DropForeignKey +ALTER TABLE "memberships" DROP CONSTRAINT "memberships_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "memberships" ADD CONSTRAINT "memberships_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "memberships" ADD CONSTRAINT "memberships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "invitations" ADD CONSTRAINT "invitations_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "memberships"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_linkId_fkey" FOREIGN KEY ("linkId") REFERENCES "links"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "digests" ADD CONSTRAINT "digests_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20230321144039_add_digest_slug/migration.sql b/prisma/migrations/20230321144039_add_digest_slug/migration.sql new file mode 100644 index 0000000..9cf79cb --- /dev/null +++ b/prisma/migrations/20230321144039_add_digest_slug/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "digests" ADD COLUMN "slug" TEXT; diff --git a/prisma/migrations/20230321144114_add_slug_unique_constraint/migration.sql b/prisma/migrations/20230321144114_add_slug_unique_constraint/migration.sql new file mode 100644 index 0000000..0da4a60 --- /dev/null +++ b/prisma/migrations/20230321144114_add_slug_unique_constraint/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[slug,teamId]` on the table `digests` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "digests_slug_teamId_key" ON "digests"("slug", "teamId"); diff --git a/prisma/migrations/20230322082759_set_digest_slug_non_nullable/migration.sql b/prisma/migrations/20230322082759_set_digest_slug_non_nullable/migration.sql new file mode 100644 index 0000000..5b1c1bc --- /dev/null +++ b/prisma/migrations/20230322082759_set_digest_slug_non_nullable/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Made the column `slug` on table `digests` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +UPDATE "digests" SET "slug" = CONCAT('digest-', id); +ALTER TABLE "digests" ALTER COLUMN "slug" SET NOT NULL; diff --git a/prisma/migrations/20230322135339_add_profile_fields/migration.sql b/prisma/migrations/20230322135339_add_profile_fields/migration.sql new file mode 100644 index 0000000..9ecd2fd --- /dev/null +++ b/prisma/migrations/20230322135339_add_profile_fields/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "teams" ADD COLUMN "bio" TEXT, +ADD COLUMN "github" TEXT, +ADD COLUMN "twitter" TEXT, +ADD COLUMN "website" TEXT; diff --git a/prisma/migrations/20230323100353_switch_to_full_uuid/migration.sql b/prisma/migrations/20230323100353_switch_to_full_uuid/migration.sql new file mode 100644 index 0000000..ae1e7fa --- /dev/null +++ b/prisma/migrations/20230323100353_switch_to_full_uuid/migration.sql @@ -0,0 +1,70 @@ +/* + Warnings: + + - The primary key for the `memberships` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `teams` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "bookmarks" DROP CONSTRAINT "bookmarks_membershipId_fkey"; + +-- DropForeignKey +ALTER TABLE "bookmarks" DROP CONSTRAINT "bookmarks_teamId_fkey"; + +-- DropForeignKey +ALTER TABLE "digests" DROP CONSTRAINT "digests_teamId_fkey"; + +-- DropForeignKey +ALTER TABLE "invitations" DROP CONSTRAINT "invitations_membershipId_fkey"; + +-- DropForeignKey +ALTER TABLE "memberships" DROP CONSTRAINT "memberships_teamId_fkey"; + +-- DropForeignKey +ALTER TABLE "users" DROP CONSTRAINT "users_defaultTeamId_fkey"; + +-- AlterTable +ALTER TABLE "bookmarks" ALTER COLUMN "membershipId" SET DATA TYPE TEXT, +ALTER COLUMN "teamId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "digests" ALTER COLUMN "teamId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "invitations" ALTER COLUMN "membershipId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "memberships" DROP CONSTRAINT "memberships_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ALTER COLUMN "teamId" SET DATA TYPE TEXT, +ADD CONSTRAINT "memberships_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "memberships_id_seq"; + +-- AlterTable +ALTER TABLE "teams" DROP CONSTRAINT "teams_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "teams_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "teams_id_seq"; + +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "defaultTeamId" SET DATA TYPE TEXT; + +-- AddForeignKey +ALTER TABLE "users" ADD CONSTRAINT "users_defaultTeamId_fkey" FOREIGN KEY ("defaultTeamId") REFERENCES "teams"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "memberships" ADD CONSTRAINT "memberships_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "invitations" ADD CONSTRAINT "invitations_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "memberships"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "memberships"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "digests" ADD CONSTRAINT "digests_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20230407083642_add_title_and_desc_to_bookmark_digest/migration.sql b/prisma/migrations/20230407083642_add_title_and_desc_to_bookmark_digest/migration.sql new file mode 100644 index 0000000..69ecb55 --- /dev/null +++ b/prisma/migrations/20230407083642_add_title_and_desc_to_bookmark_digest/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "bookmark_digest" ADD COLUMN "description" TEXT, +ADD COLUMN "title" TEXT; diff --git a/prisma/migrations/20230412121851_add_bookmarkdigest_style/migration.sql b/prisma/migrations/20230412121851_add_bookmarkdigest_style/migration.sql new file mode 100644 index 0000000..806e24f --- /dev/null +++ b/prisma/migrations/20230412121851_add_bookmarkdigest_style/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "BookmarkDigestStyle" AS ENUM ('BLOCK', 'INLINE'); + +-- AlterTable +ALTER TABLE "bookmark_digest" ADD COLUMN "style" "BookmarkDigestStyle" NOT NULL DEFAULT 'BLOCK'; diff --git a/prisma/migrations/20230428093324_add_typefully_key/migration.sql b/prisma/migrations/20230428093324_add_typefully_key/migration.sql new file mode 100644 index 0000000..8a8ca18 --- /dev/null +++ b/prisma/migrations/20230428093324_add_typefully_key/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "teams" ADD COLUMN "typefullyToken" TEXT; diff --git a/prisma/migrations/20230505145858_add_typefully_thread_id/migration.sql b/prisma/migrations/20230505145858_add_typefully_thread_id/migration.sql new file mode 100644 index 0000000..4f1f544 --- /dev/null +++ b/prisma/migrations/20230505145858_add_typefully_thread_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "digests" ADD COLUMN "typefullyThreadId" TEXT; diff --git a/prisma/migrations/20230524124748_rename_typefully_thread_id_to_thread_url/migration.sql b/prisma/migrations/20230524124748_rename_typefully_thread_id_to_thread_url/migration.sql new file mode 100644 index 0000000..d9cb448 --- /dev/null +++ b/prisma/migrations/20230524124748_rename_typefully_thread_id_to_thread_url/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `typefullyThreadId` on the `digests` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "digests" DROP COLUMN "typefullyThreadId", +ADD COLUMN "typefullyThreadUrl" TEXT; diff --git a/prisma/migrations/20230530095640_added_subscription/migration.sql b/prisma/migrations/20230530095640_added_subscription/migration.sql new file mode 100644 index 0000000..70d9129 --- /dev/null +++ b/prisma/migrations/20230530095640_added_subscription/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "subscriptions" ( + "id" TEXT NOT NULL, + "teamId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "expirationTime" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20230530100155_update_subscription/migration.sql b/prisma/migrations/20230530100155_update_subscription/migration.sql new file mode 100644 index 0000000..5ded6c5 --- /dev/null +++ b/prisma/migrations/20230530100155_update_subscription/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `expirationTime` on the `subscriptions` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "subscriptions" DROP COLUMN "expirationTime"; diff --git a/prisma/migrations/20230531144239_add_hassentnewsletter_field/migration.sql b/prisma/migrations/20230531144239_add_hassentnewsletter_field/migration.sql new file mode 100644 index 0000000..a6d9e27 --- /dev/null +++ b/prisma/migrations/20230531144239_add_hassentnewsletter_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "digests" ADD COLUMN "hasSentNewsletter" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20230605160311_add_text_block_in_digest/migration.sql b/prisma/migrations/20230605160311_add_text_block_in_digest/migration.sql new file mode 100644 index 0000000..fb412c3 --- /dev/null +++ b/prisma/migrations/20230605160311_add_text_block_in_digest/migration.sql @@ -0,0 +1,7 @@ +-- CreateEnum +CREATE TYPE "DigestBlockType" AS ENUM ('BOOKMARK', 'TEXT'); + +-- AlterTable +ALTER TABLE "bookmark_digest" ADD COLUMN "DigestBlockType" "DigestBlockType" NOT NULL DEFAULT 'BOOKMARK', +ADD COLUMN "text" TEXT, +ALTER COLUMN "bookmarkId" DROP NOT NULL; diff --git a/prisma/migrations/20230606094441_rename/migration.sql b/prisma/migrations/20230606094441_rename/migration.sql new file mode 100644 index 0000000..06b7b3b --- /dev/null +++ b/prisma/migrations/20230606094441_rename/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `DigestBlockType` on the `bookmark_digest` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "bookmark_digest" DROP COLUMN "DigestBlockType", +ADD COLUMN "type" "DigestBlockType" NOT NULL DEFAULT 'BOOKMARK'; diff --git a/prisma/migrations/20230609100206_rename_bookmark_digests_to_digest_block/migration.sql b/prisma/migrations/20230609100206_rename_bookmark_digests_to_digest_block/migration.sql new file mode 100644 index 0000000..d9ea68d --- /dev/null +++ b/prisma/migrations/20230609100206_rename_bookmark_digests_to_digest_block/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "bookmark_digest" RENAME TO "digest_blocks"; \ No newline at end of file diff --git a/prisma/migrations/20230609121534_update_digest_block_relationships/migration.sql b/prisma/migrations/20230609121534_update_digest_block_relationships/migration.sql new file mode 100644 index 0000000..d798074 --- /dev/null +++ b/prisma/migrations/20230609121534_update_digest_block_relationships/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable +ALTER TABLE "digest_blocks" RENAME CONSTRAINT "bookmark_digest_pkey" TO "digest_blocks_pkey"; + +-- RenameForeignKey +ALTER TABLE "digest_blocks" RENAME CONSTRAINT "bookmark_digest_bookmarkId_fkey" TO "digest_blocks_bookmarkId_fkey"; + +-- RenameForeignKey +ALTER TABLE "digest_blocks" RENAME CONSTRAINT "bookmark_digest_digestId_fkey" TO "digest_blocks_digestId_fkey"; + +-- RenameIndex +ALTER INDEX "bookmark_digest_bookmarkId_digestId_key" RENAME TO "digest_blocks_bookmarkId_digestId_key"; diff --git a/prisma/migrations/20230612095908_add_created_at_to_teams/migration.sql b/prisma/migrations/20230612095908_add_created_at_to_teams/migration.sql new file mode 100644 index 0000000..30806a7 --- /dev/null +++ b/prisma/migrations/20230612095908_add_created_at_to_teams/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "teams" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/migrations/20230620145041_add_api_key/migration.sql b/prisma/migrations/20230620145041_add_api_key/migration.sql new file mode 100644 index 0000000..5aa0d64 --- /dev/null +++ b/prisma/migrations/20230620145041_add_api_key/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "teams" ADD COLUMN "apiKey" TEXT; diff --git a/prisma/migrations/20230818095838_add_logo_to_link/migration.sql b/prisma/migrations/20230818095838_add_logo_to_link/migration.sql new file mode 100644 index 0000000..3fdb79d --- /dev/null +++ b/prisma/migrations/20230818095838_add_logo_to_link/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "links" ADD COLUMN "logo" TEXT; diff --git a/prisma/migrations/20230821114929_add_tweet_embed_style/migration.sql b/prisma/migrations/20230821114929_add_tweet_embed_style/migration.sql new file mode 100644 index 0000000..14e87e1 --- /dev/null +++ b/prisma/migrations/20230821114929_add_tweet_embed_style/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "BookmarkDigestStyle" ADD VALUE 'TWEET_EMBED'; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..a50aa8c --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,224 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator jsonSchema { + provider = "prisma-json-schema-generator" +} + +enum MembershipRole { + ADMIN + USER +} + +enum GlobalRole { + SUPERADMIN + USER +} + +enum Provider { + WEB + SLACK +} + +enum BookmarkDigestStyle { + BLOCK + INLINE + TWEET_EMBED +} + +enum DigestBlockType { + BOOKMARK + TEXT +} + +model Account { + id String @id @default(uuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@map("accounts") +} + +model Session { + id String @id @default(uuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("sessions") +} + +model User { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + role GlobalRole @default(USER) + memberships Membership[] + defaultTeam Team? @relation(fields: [defaultTeamId], references: [id], onDelete: SetNull) + defaultTeamId String? + + @@map("users") +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) + @@map("verification_tokens") +} + +model Team { + id String @id @default(uuid()) + name String + slug String + memberships Membership[] + subscriptions Subscription[] + users User[] + Digest Digest[] + bookmarks Bookmark[] + slackToken String? + slackTeamId String? + typefullyToken String? + bio String? + website String? + twitter String? + github String? + createdAt DateTime @default(now()) + apiKey String? + + @@unique([id, slackTeamId]) + @@unique(slug) + @@map("teams") +} + +model Membership { + id String @id @default(uuid()) + role MembershipRole + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + teamId String + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? + invitedName String? + invitedEmail String? + invitations Invitation[] + bookmark Bookmark[] + + @@unique([teamId, invitedEmail]) + @@map("memberships") +} + +model Invitation { + id String @id @default(uuid()) + membership Membership @relation(fields: [membershipId], references: [id], onDelete: Cascade) + membershipId String + expiredAt DateTime + createdAt DateTime @default(now()) + validatedAt DateTime? + + @@map("invitations") +} + +model Link { + id String @id @default(uuid()) + url String + image String? + blurHash String? + title String? + logo String? + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + bookmark Bookmark[] + + @@map("links") +} + +model Bookmark { + id String @id @default(uuid()) + link Link @relation(fields: [linkId], references: [id], onDelete: Cascade) + linkId String + membership Membership? @relation(fields: [membershipId], references: [id], onDelete: SetNull) + membershipId String? + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + teamId String + provider Provider @default(WEB) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + digestBlocks DigestBlock[] + metadata Json? + + @@map("bookmarks") +} + +model Digest { + id String @id @default(uuid()) + title String + slug String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + publishedAt DateTime? + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + teamId String + digestBlocks DigestBlock[] + typefullyThreadUrl String? + hasSentNewsletter Boolean @default(false) + + @@unique([slug, teamId]) + @@map("digests") +} + + +model DigestBlock { + id String @id @default(uuid()) + bookmark Bookmark? @relation(fields : [bookmarkId], references : [id], onDelete: Cascade) + bookmarkId String? + digest Digest @relation(fields : [digestId], references: [id], onDelete: Cascade) + digestId String + order Int + style BookmarkDigestStyle @default(BLOCK) + title String? + description String? + type DigestBlockType @default(BOOKMARK) + text String? + + @@unique([bookmarkId, digestId]) + @@map("digest_blocks") +} + +model Subscription { + id String @id @default(uuid()) + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + teamId String + email String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("subscriptions") +} diff --git a/public/changelogs/changelog-001.webp b/public/changelogs/changelog-001.webp new file mode 100644 index 0000000..a6f9e89 Binary files /dev/null and b/public/changelogs/changelog-001.webp differ diff --git a/public/changelogs/changelog-002.webp b/public/changelogs/changelog-002.webp new file mode 100644 index 0000000..48b6bcf Binary files /dev/null and b/public/changelogs/changelog-002.webp differ diff --git a/public/changelogs/changelog-003.webp b/public/changelogs/changelog-003.webp new file mode 100644 index 0000000..9ab3fc7 Binary files /dev/null and b/public/changelogs/changelog-003.webp differ diff --git a/public/changelogs/changelog-004.webp b/public/changelogs/changelog-004.webp new file mode 100644 index 0000000..72f7188 Binary files /dev/null and b/public/changelogs/changelog-004.webp differ diff --git a/public/changelogs/changelog-005.webp b/public/changelogs/changelog-005.webp new file mode 100644 index 0000000..9058f58 Binary files /dev/null and b/public/changelogs/changelog-005.webp differ diff --git a/public/collect.svg b/public/collect.svg new file mode 100644 index 0000000..a00208f --- /dev/null +++ b/public/collect.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..51188c9 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/hero.svg b/public/hero.svg new file mode 100644 index 0000000..899a0a1 --- /dev/null +++ b/public/hero.svg @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logo-digest.png b/public/logo-digest.png new file mode 100644 index 0000000..e30f74d Binary files /dev/null and b/public/logo-digest.png differ diff --git a/public/og-cover.png b/public/og-cover.png new file mode 100644 index 0000000..44fbcc1 Binary files /dev/null and b/public/og-cover.png differ diff --git a/public/organize.svg b/public/organize.svg new file mode 100644 index 0000000..05b53b7 --- /dev/null +++ b/public/organize.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/publish.svg b/public/publish.svg new file mode 100644 index 0000000..0eb467f --- /dev/null +++ b/public/publish.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/slack.svg b/public/slack.svg new file mode 100644 index 0000000..1bc645b --- /dev/null +++ b/public/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/thread.svg b/public/thread.svg new file mode 100644 index 0000000..1846fea --- /dev/null +++ b/public/thread.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + diff --git a/public/typefully.svg b/public/typefully.svg new file mode 100644 index 0000000..a27c611 --- /dev/null +++ b/public/typefully.svg @@ -0,0 +1,60 @@ + + + + + + + + diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..fbf0e25 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/sentry.client.config.ts b/sentry.client.config.ts new file mode 100644 index 0000000..f835d4a --- /dev/null +++ b/sentry.client.config.ts @@ -0,0 +1,28 @@ +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + enabled: process.env.NODE_ENV !== 'development', + environment: process.env.VERCEL_ENV, + debug: false, + + replaysOnErrorSampleRate: 1.0, + + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + integrations: [ + new Sentry.Replay({ + // Additional Replay configuration goes in here, for example: + maskAllText: true, + blockAllMedia: true, + }), + ], +}); diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts new file mode 100644 index 0000000..1d66e29 --- /dev/null +++ b/sentry.edge.config.ts @@ -0,0 +1,12 @@ +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + enabled: process.env.NODE_ENV !== 'development', +}); diff --git a/sentry.properties b/sentry.properties new file mode 100644 index 0000000..9939593 --- /dev/null +++ b/sentry.properties @@ -0,0 +1,4 @@ +defaults.url=https://sentry.io/ +defaults.org=premier-octet-z6 +defaults.project=digestclub +cli.executable=node_modules/@sentry/cli/bin/sentry-cli diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 0000000..f1073dc --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,13 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; +import db from '@/lib/db'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + enabled: process.env.NODE_ENV !== 'development', + integrations: [new Sentry.Integrations.Prisma({ client: db })], +}); diff --git a/src/app/(routes)/[teamSlug]/(feed)/atom.xml/route.ts b/src/app/(routes)/[teamSlug]/(feed)/atom.xml/route.ts new file mode 100644 index 0000000..18f1942 --- /dev/null +++ b/src/app/(routes)/[teamSlug]/(feed)/atom.xml/route.ts @@ -0,0 +1,15 @@ +import { getPublicTeam } from '@/lib/queries'; +import { NextRequest, NextResponse } from 'next/server'; +import { atom } from '@/utils/feed'; + +export async function GET( + _request: NextRequest, + { params: { teamSlug } }: { params: { teamSlug: string } } +) { + const team = await getPublicTeam(teamSlug); + return new NextResponse(atom(team, teamSlug), { + headers: { + 'content-type': 'application/atom+xml; charset=utf-8', + }, + }); +} diff --git a/src/app/(routes)/[teamSlug]/(feed)/rss.xml/route.ts b/src/app/(routes)/[teamSlug]/(feed)/rss.xml/route.ts new file mode 100644 index 0000000..ef7727f --- /dev/null +++ b/src/app/(routes)/[teamSlug]/(feed)/rss.xml/route.ts @@ -0,0 +1,15 @@ +import { getPublicTeam } from '@/lib/queries'; +import { NextRequest, NextResponse } from 'next/server'; +import { rss } from '@/utils/feed'; + +export async function GET( + _request: NextRequest, + { params: { teamSlug } }: { params: { teamSlug: string } } +) { + const team = await getPublicTeam(teamSlug); + return new NextResponse(rss(team, teamSlug), { + headers: { + 'content-type': 'application/rss+xml; charset=utf-8', + }, + }); +} diff --git a/src/app/(routes)/[teamSlug]/[digestSlug]/page.tsx b/src/app/(routes)/[teamSlug]/[digestSlug]/page.tsx new file mode 100644 index 0000000..e2890a2 --- /dev/null +++ b/src/app/(routes)/[teamSlug]/[digestSlug]/page.tsx @@ -0,0 +1,57 @@ +import DigestPublicPage from '@/components/pages/DigestPublicPage'; +import { getPublicDigest } from '@/lib/queries'; +import { generateDigestOGUrl } from '@/utils/open-graph'; +import { Metadata } from 'next'; +import { redirect } from 'next/navigation'; +import * as Sentry from '@sentry/nextjs'; + +interface PageProps { + params: { teamSlug: string; digestSlug: string }; +} + +export async function generateMetadata({ + params, +}: PageProps): Promise { + try { + const digest = await getPublicDigest(params.digestSlug, params.teamSlug); + const url = generateDigestOGUrl(params.digestSlug); + + return { + title: `${digest?.title} by ${digest?.team.name}`, + twitter: { + card: 'summary_large_image', + title: `${digest?.title}`, + description: digest?.description || digest?.team.name, + images: [url], + }, + openGraph: { + type: 'website', + title: `${digest?.title}`, + description: digest?.description || digest?.team.name, + url, + images: [ + { + url, + width: 1200, + height: 600, + }, + ], + }, + }; + } catch (error) { + Sentry.captureException(error); + return {}; + } +} + +const PublicDigestPage = async ({ params }: PageProps) => { + const digest = await getPublicDigest(params.digestSlug, params.teamSlug); + + if (!digest) { + redirect('/'); + } + + return ; +}; + +export default PublicDigestPage; diff --git a/src/app/(routes)/[teamSlug]/page.tsx b/src/app/(routes)/[teamSlug]/page.tsx new file mode 100644 index 0000000..29513ab --- /dev/null +++ b/src/app/(routes)/[teamSlug]/page.tsx @@ -0,0 +1,58 @@ +import TeamPublicPage from '@/components/pages/TeamPublicPage'; +import { getPublicTeam } from '@/lib/queries'; +import { getEnvHost } from '@/lib/server'; +import { generateTeamOG } from '@/utils/open-graph'; +import { Metadata } from 'next'; +import { redirect } from 'next/navigation'; + +interface PageProps { + params: { teamSlug: string }; +} + +export async function generateMetadata({ + params, +}: PageProps): Promise { + const team = await getPublicTeam(params.teamSlug); + const url = generateTeamOG(team?.slug || ''); + + return { + alternates: { + types: { + 'application/rss+xml': `${getEnvHost()}/${params.teamSlug}/rss.xml`, + 'application/atom+xml': `${getEnvHost()}/${params.teamSlug}/atom.xml`, + }, + }, + title: `${team?.name}`, + twitter: { + card: 'summary_large_image', + title: `${team?.name}`, + description: team?.bio || team?.name, + images: [url], + }, + openGraph: { + type: 'website', + title: `${team?.name}`, + description: team?.bio || team?.name, + url, + images: [ + { + url, + width: 1200, + height: 600, + }, + ], + }, + }; +} + +const PublicTeamPage = async ({ params }: PageProps) => { + const team = await getPublicTeam(params.teamSlug); + + if (!team) { + redirect('/'); + } + + return ; +}; + +export default PublicTeamPage; diff --git a/src/app/(routes)/account/page.tsx b/src/app/(routes)/account/page.tsx new file mode 100644 index 0000000..aaa9433 --- /dev/null +++ b/src/app/(routes)/account/page.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { getCurrentUser } from '@/lib/sessions'; +import PageContainer from '@/components/layout/PageContainer'; +import AccountForm from '@/components/account/AccountForm'; +import SectionContainer from '@/components/layout/SectionContainer'; +import { notFound } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; +export const metadata = { + title: 'My account', +}; + +const AccountPage = async () => { + const user = await getCurrentUser(); + if (!user) return notFound(); + + return ( + + + + + + ); +}; + +export default AccountPage; diff --git a/src/app/(routes)/auth/layout.tsx b/src/app/(routes)/auth/layout.tsx new file mode 100644 index 0000000..6196962 --- /dev/null +++ b/src/app/(routes)/auth/layout.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { CheckCircleIcon } from '@heroicons/react/24/solid'; + +type Props = { + children: ReactNode; +}; + +export default function Layout({ children }: Props) { + const features = [ + 'Collect links', + 'Design your digest', + 'Share your digests with tech folks!', + ]; + return ( +
+
+
+ Join the Club 👋 + + Digest Club helps your team to share the knowledge! + +
    + {features.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+
+
{children}
+
+
+
+ ); +} diff --git a/src/app/(routes)/auth/login/page.tsx b/src/app/(routes)/auth/login/page.tsx new file mode 100644 index 0000000..e35a842 --- /dev/null +++ b/src/app/(routes)/auth/login/page.tsx @@ -0,0 +1,21 @@ +import LoginForm from '@/components/auth/LoginForm'; +import { routes } from '@/core/constants'; +import { getSession } from '@/lib/sessions'; +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; +export const metadata = { + title: 'Login', +}; + +const Login = async () => { + const session = await getSession(); + + if (session) { + redirect(routes.TEAMS); + } + + return ; +}; + +export default Login; diff --git a/src/app/(routes)/invitations/[invitationId]/accept/page.tsx b/src/app/(routes)/invitations/[invitationId]/accept/page.tsx new file mode 100644 index 0000000..80dd605 --- /dev/null +++ b/src/app/(routes)/invitations/[invitationId]/accept/page.tsx @@ -0,0 +1,47 @@ +import Invitation from '@/components/pages/Invitation'; +import db from '@/lib/db'; +import { getCurrentUserOrRedirect } from '@/lib/sessions'; +import { notFound } from 'next/navigation'; + +interface InvitationPageProps { + params: { invitationId: string }; +} + +export const metadata = { + title: 'Invitation', +}; + +const InvitationPage = async ({ params }: InvitationPageProps) => { + const { invitationId } = params; + const user = await getCurrentUserOrRedirect(); + + const invitation = await db.invitation.findUnique({ + select: { + id: true, + membershipId: true, + membership: { + include: { + team: true, + }, + }, + }, + where: { + id: invitationId!.toString(), + }, + }); + + if (!invitation) { + return notFound(); + } + + return ( + + ); +}; + +export default InvitationPage; diff --git a/src/app/(routes)/layout.tsx b/src/app/(routes)/layout.tsx new file mode 100644 index 0000000..7873bc7 --- /dev/null +++ b/src/app/(routes)/layout.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +type Props = { + children: React.ReactNode; +}; + +const Layout = ({ children }: Props) => { + return ( +
+ {children} +
+ ); +}; + +export default Layout; diff --git a/src/app/(routes)/teams/[teamSlug]/digests/[digestId]/edit/page.tsx b/src/app/(routes)/teams/[teamSlug]/digests/[digestId]/edit/page.tsx new file mode 100644 index 0000000..402b420 --- /dev/null +++ b/src/app/(routes)/teams/[teamSlug]/digests/[digestId]/edit/page.tsx @@ -0,0 +1,53 @@ +import { DigestEditPage } from '@/components/pages/DigestEditPage'; +import { TeamProvider } from '@/contexts/TeamContext'; +import { + checkUserTeamBySlug, + getDigest, + getTeamBookmarksNotInDigest, + updateDefaultTeam, +} from '@/lib/queries'; +import { getCurrentUserOrRedirect } from '@/lib/sessions'; +import { redirect } from 'next/navigation'; + +export interface TeamPageProps { + params: { teamSlug: string; digestId: string }; + searchParams?: { [key: string]: string | undefined }; +} + +const page = async ({ params, searchParams }: TeamPageProps) => { + const user = await getCurrentUserOrRedirect(); + const team = await checkUserTeamBySlug(params.teamSlug, user.id); + + if (!team) { + redirect('/teams'); + } + + await updateDefaultTeam(user.id, team.id); + + const digest = await getDigest(params.digestId); + + if (!digest || digest.teamId !== team.id) { + redirect(`/teams/${team.slug}/digests/${params.digestId}`); + } + + const page = Number(searchParams?.page || 1); + const search = searchParams?.search || ''; + const dataBookmarks = await getTeamBookmarksNotInDigest( + team.id, + page, + 10, + search + ); + + return ( + + + + ); +}; + +export default page; diff --git a/src/app/(routes)/teams/[teamSlug]/layout.tsx b/src/app/(routes)/teams/[teamSlug]/layout.tsx new file mode 100644 index 0000000..93b21e7 --- /dev/null +++ b/src/app/(routes)/teams/[teamSlug]/layout.tsx @@ -0,0 +1,17 @@ +import { getCurrentUser } from '@/lib/sessions'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; +import { redirect } from 'next/navigation'; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const user = await getCurrentUser(); + + if (!user) { + redirect(authOptions.pages!.signIn!); + } + + return children; +} diff --git a/src/app/(routes)/teams/[teamSlug]/page.tsx b/src/app/(routes)/teams/[teamSlug]/page.tsx new file mode 100644 index 0000000..75db70b --- /dev/null +++ b/src/app/(routes)/teams/[teamSlug]/page.tsx @@ -0,0 +1,58 @@ +import Team from '@/components/pages/Team'; +import db from '@/lib/db'; +import { + checkUserTeamBySlug, + getTeamBookmarks, + getTeamDigests, + updateDefaultTeam, +} from '@/lib/queries'; +import { getCurrentUserOrRedirect } from '@/lib/sessions'; +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; + +export interface TeamPageProps { + params: { teamSlug: string }; + searchParams?: { [key: string]: string | undefined }; +} + +export async function generateMetadata({ + params, +}: TeamPageProps): Promise { + const team = await db.team.findFirst({ + select: { name: true }, + where: { + slug: params.teamSlug, + }, + }); + if (!team) throw notFound(); + return { title: `Team ${team.name}` }; +} + +const TeamPage = async ({ params, searchParams }: TeamPageProps) => { + const user = await getCurrentUserOrRedirect(); + const team = await checkUserTeamBySlug(params.teamSlug, user.id); + if (!team) { + throw notFound(); + } + + await updateDefaultTeam(user.id, team.id); + + const page = Number(searchParams?.page || 1); + const { totalCount, bookmarks } = await getTeamBookmarks(team.id, { + page, + onlyNotInDigest: !searchParams?.all, + }); + + const digests = await getTeamDigests(team.id, 1, 11); + + return ( + + ); +}; + +export default TeamPage; diff --git a/src/app/(routes)/teams/[teamSlug]/settings/page.tsx b/src/app/(routes)/teams/[teamSlug]/settings/page.tsx new file mode 100644 index 0000000..ab85e4e --- /dev/null +++ b/src/app/(routes)/teams/[teamSlug]/settings/page.tsx @@ -0,0 +1,42 @@ +import { TeamSettings } from '@/components/teams/TeamSettings'; +import { + checkUserTeamBySlug, + getTeamInvitations, + getTeamMembers, + getTeamSubscriptions, +} from '@/lib/queries'; +import { getCurrentUser } from '@/lib/sessions'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; +import { notFound, redirect } from 'next/navigation'; +import { TeamPageProps } from '../page'; + +const TeamSettingsPage = async ({ params }: TeamPageProps) => { + const teamSlug = params.teamSlug; + const user = await getCurrentUser(); + if (!user) { + return redirect(authOptions.pages!.signIn!); + } + + const team = await checkUserTeamBySlug(teamSlug, user.id); + + if (!team) { + redirect('/teams'); + } + + const members = await getTeamMembers(teamSlug); + const invitations = await getTeamInvitations(teamSlug); + const subscriptions = await getTeamSubscriptions(teamSlug); + + if (!user?.id) return notFound(); + return ( + + ); +}; + +export default TeamSettingsPage; diff --git a/src/app/(routes)/teams/create/page.tsx b/src/app/(routes)/teams/create/page.tsx new file mode 100644 index 0000000..0ec4c0e --- /dev/null +++ b/src/app/(routes)/teams/create/page.tsx @@ -0,0 +1,21 @@ +import CreateTeam from '@/components/teams/form/CreateTeam'; +export const dynamic = 'force-dynamic'; + +const CreatePage = () => { + return ( +
+
+
+

+ Create New Team +

+
+
+ +
+
+
+ ); +}; + +export default CreatePage; diff --git a/src/app/(routes)/teams/layout.tsx b/src/app/(routes)/teams/layout.tsx new file mode 100644 index 0000000..93b21e7 --- /dev/null +++ b/src/app/(routes)/teams/layout.tsx @@ -0,0 +1,17 @@ +import { getCurrentUser } from '@/lib/sessions'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; +import { redirect } from 'next/navigation'; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const user = await getCurrentUser(); + + if (!user) { + redirect(authOptions.pages!.signIn!); + } + + return children; +} diff --git a/src/app/(routes)/teams/page.tsx b/src/app/(routes)/teams/page.tsx new file mode 100644 index 0000000..1bc9887 --- /dev/null +++ b/src/app/(routes)/teams/page.tsx @@ -0,0 +1,28 @@ +import { routes } from '@/core/constants'; +import { getUserTeams } from '@/lib/queries'; +import { getSession } from '@/lib/sessions'; +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export const metadata = { + title: 'Team', +}; + +const AppPage = async () => { + const session = await getSession(); + + if (!session) { + return redirect(routes.LOGIN); + } + + const teams = await getUserTeams(session.user.id); + + if (teams.length === 0) { + redirect(routes.TEAMS_CREATE); + } else { + redirect(routes.TEAM.replace(':slug', teams[0].slug)); + } +}; + +export default AppPage; diff --git a/src/app/(routes)/unsubscribe/page.tsx b/src/app/(routes)/unsubscribe/page.tsx new file mode 100644 index 0000000..8842c2b --- /dev/null +++ b/src/app/(routes)/unsubscribe/page.tsx @@ -0,0 +1,36 @@ +import UnsubscribeConfirmation from '@/components/newsletter/UnsubscribeConfirmation'; +import db from '@/lib/db'; +import { notFound } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default async function Page({ + searchParams, +}: { + searchParams: { [key: string]: string | undefined }; +}) { + if (!searchParams.email || !searchParams.teamId) notFound(); + const { email, teamId } = searchParams; + + const team = await db.team.findUnique({ + where: { + id: teamId, + }, + select: { + name: true, + }, + }); + if (!team) notFound(); + + return ( +
+
+ +
+
+ ); +} diff --git a/src/app/[...not_found]/page.tsx b/src/app/[...not_found]/page.tsx new file mode 100644 index 0000000..7cbaad6 --- /dev/null +++ b/src/app/[...not_found]/page.tsx @@ -0,0 +1,7 @@ +import Link from 'next/link'; +import { notFound } from 'next/navigation'; + +export default function NotFoundCatchAll() { + notFound(); + return null; +} diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..db756d8 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,29 @@ +'use client'; +import Link from 'next/link'; +import React from 'react'; + +export default function InternalErrorPage() { + return ( + <> +
+
+

500

+

+ Something went wrong! +

+

+ Sorry for the inconvenience, we are working on it. +

+
+ + Go back home + +
+
+
+ + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..f136ff6 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,66 @@ +import Header from '@/components/layout/Header'; +import { getUserTeams } from '@/lib/queries'; +import { getSession } from '@/lib/sessions'; +import { getEnvHost } from '@/lib/server'; +import { Metadata } from 'next'; +import '../theme/app.css'; +import '@/theme/globals.css'; +import Providers from './providers'; + +export const dynamic = 'force-dynamic'; + +type Props = { + children: React.ReactNode; +}; + +const description = + 'The Frontpage of Teams Knowledge. Save and share your team’s curation.'; +const title = `Digest.club`; + +export const metadata: Metadata = { + icons: { icon: '/favicon.ico' }, + title: { + default: 'Digest.club - The Frontpage of Teams Knowledge', + template: '%s | Digest.club', + }, + description, + twitter: { + card: 'summary_large_image', + title, + description, + images: [`${getEnvHost()}/og-cover.png`], + }, + openGraph: { + title, + description, + url: process.env.NEXTAUTH_URL, + siteName: title, + images: [ + { + url: `${getEnvHost()}/og-cover.png`, + width: 2400, + height: 1200, + }, + ], + locale: 'en-GB', + type: 'website', + }, +}; + +export default async function RootLayout({ children }: Props) { + const session = await getSession(); + const teams = session?.user.id + ? await getUserTeams(session.user.id) + : undefined; + + return ( + + + +
+ {children} + + + + ); +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..50113ea --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,29 @@ +import Link from 'next/link'; +import React from 'react'; +export const dynamic = 'force-dynamic'; + +export default function NotFound() { + return ( + <> +
+
+

404

+

+ Page not found +

+

+ Sorry, we couldn’t find the page you’re looking for. +

+
+ + Go back home + +
+
+
+ + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..bc12fc0 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,10 @@ +import Homepage from '@/components/pages/Homepage'; +import { getCurrentUser } from '@/lib/sessions'; +export const dynamic = 'force-dynamic'; + +const Home = async () => { + await getCurrentUser(); + return ; +}; + +export default Home; diff --git a/src/app/providers.tsx b/src/app/providers.tsx new file mode 100644 index 0000000..4376465 --- /dev/null +++ b/src/app/providers.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { Toaster } from 'react-hot-toast'; +import { SessionProvider } from 'next-auth/react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import messages from '../messages/en.json'; + +const locale = 'en'; + +const queryClient = new QueryClient(); + +export default function Providers({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + + ); +} diff --git a/src/app/updates/page.tsx b/src/app/updates/page.tsx new file mode 100644 index 0000000..cbdafe9 --- /dev/null +++ b/src/app/updates/page.tsx @@ -0,0 +1,41 @@ +import ChangelogPost from '@/components/changelog/ChangelogPost'; +import { Metadata } from 'next'; +import { allChangelogs, Changelog } from 'contentlayer/generated'; +import HomeFooter from '@/components/home/HomeFooter'; + +export const dynamic = 'force-static'; + +export const metadata: Metadata = { + title: 'Changelog', + description: + 'All the latest updates, improvements, and fixes to Digest.club.', +}; + +export default async function Updates() { + return ( +
+
+
+

+ Updates +

+
+ {allChangelogs + .sort((a, b) => { + if (new Date(a.publishedAt) > new Date(b.publishedAt)) { + return -1; + } + return 1; + }) + .map((changelog, i) => ( +
+ +
+ ))} +
+
+
+ +
+ ); +} diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx new file mode 100644 index 0000000..1eebd66 --- /dev/null +++ b/src/components/Avatar.tsx @@ -0,0 +1,66 @@ +import Image from 'next/image'; +import React from 'react'; + +interface IProps { + size: 'xs' | 'sm' | 'md' | 'lg'; + name?: string; + src?: string; +} + +export default function Avatar({ size = 'md', src, name }: IProps) { + const sizeClass: Record = { + xs: 'h-4 w-4', + sm: 'h-6 w-6', + md: 'h-8 w-8', + lg: 'h-10 w-10', + }; + + const sizePixels: Record = { + xs: 4, + sm: 6, + md: 8, + lg: 10, + }; + + if (src !== undefined) { + return ( + avatar + ); + } + + if (name !== undefined) { + return ( + + + {name + .split(' ') + .splice(0, 2) + .map((n) => n[0]) + .join('')} + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx new file mode 100644 index 0000000..0fc2358 --- /dev/null +++ b/src/components/Bookmarks.tsx @@ -0,0 +1,161 @@ +'use client'; + +import useCustomToast from '@/hooks/useCustomToast'; +import useTransitionRefresh from '@/hooks/useTransitionRefresh'; +import api from '@/lib/api'; +import { TeamBookmarksResult } from '@/lib/queries'; +import { ApiBookmarkResponseSuccess } from '@/pages/api/teams/[teamId]/bookmark'; +import { getRelativeDate } from '@/utils/date'; +import { getDomainFromUrl } from '@/utils/url'; +import { BsFillBookmarkFill } from '@react-icons/all-files/bs/BsFillBookmarkFill'; +import { AxiosError, AxiosResponse } from 'axios'; +import clsx from 'clsx'; +import Link from 'next/link'; +import { useMutation } from 'react-query'; +import NoContent from './layout/NoContent'; +import BookmarkImage from './link/BookmarkImage'; +import { DeletePopover } from './Popover'; +import message from '../messages/en'; +import { getEnvHost } from '@/lib/server'; + +type Props = { + bookmarks: TeamBookmarksResult[]; + teamId: string; + teamSlug: string; +}; + +/** + * Displays a list of bookmarks from a team + */ +export const Bookmarks = ({ bookmarks, teamId, teamSlug }: Props) => { + const { successToast, errorToast } = useCustomToast(); + const { isRefreshing, refresh } = useTransitionRefresh(); + + const { mutate: deleteBookmark, isLoading } = useMutation< + AxiosResponse, + AxiosError, + TeamBookmarksResult + >( + 'delete-bookmarks', + (bookmark) => { + return api.delete(`/teams/${teamId}/bookmark/${bookmark.id}`); + }, + { + onSuccess: () => { + successToast(message.invitation.delete.success); + refresh(); + }, + onError: (error: AxiosError) => { + errorToast( + error.response?.data?.error || + error.response?.statusText || + error.message + ); + }, + } + ); + + if (bookmarks.length < 1) { + return ( + } + title="No bookmark" + subtitle="Start bookmarking links to share them with your team" + /> + ); + } + + return ( +
+ {bookmarks.map((bookmark) => { + const isUsed = bookmark.digestBlocks.length > 0; + const nbOfTimesUsed = bookmark.digestBlocks.length; + return ( +
+ {isUsed && ( + + Bookmarked {nbOfTimesUsed > 1 ? nbOfTimesUsed : ''}{' '} + + )} +
+
+
+ +
+
+
+ + + {bookmark.link.title || bookmark.link.url} + + +
+ {bookmark.membership ? ( +
+ {bookmark.membership.user?.name || + bookmark.membership.user?.email?.split('@')[0]}{' '} + {bookmark.createdAt && + getRelativeDate(bookmark.createdAt)} +
+ ) : ( +
+ {bookmark.provider === 'SLACK' && ( + <> + From Slack{' '} + {bookmark.createdAt && + getRelativeDate(bookmark.createdAt)} + + )} +
+ )} +
-
+
+ {getDomainFromUrl(bookmark.link.url)} +
+
-
+
+ deleteBookmark(bookmark)} + isLoading={isLoading || isRefreshing} + /> +
+
+
+
+
+
+
+ ); + })} +
+ ); +}; diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..81aea2d --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,131 @@ +'use client'; + +import React, { forwardRef } from 'react'; +import { AiOutlineLoading3Quarters as LoadingIcon } from '@react-icons/all-files/ai/AiOutlineLoading3Quarters'; +import clsx from 'clsx'; +import { VariantProps, cva } from 'class-variance-authority'; + +const buttonVariants = cva( + 'rounded-md font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 flex items-center justify-center gap-2 cursor-pointer', + { + variants: { + variant: { + default: + 'ring-1 shadow-sm bg-violet-600 hover:bg-violet-700 text-white ring-violet-600', + outline: + 'ring-1 shadow-sm bg-transparent hover:bg-violet-700 text-violet-700 ring-1 ring-violet-600 hover:bg-violet-100 hover:text-white', + destructive: + 'ring-1 shadow-sm bg-red-700 hover:bg-red-600 text-white ring-red-700', + destructiveOutline: + 'ring-1 shadow-sm bg-transparent hover:bg-red-700 text-red-700 ring-1 ring-red-700 hover:bg-red-100 hover:text-white', + success: + 'ring-1 shadow-sm bg-[#4ade80] hover:bg-[#3dba6b] text-white ring-[#4ade80] hover:ring-[#3dba6b]', + ghost: + 'bg-transparent text-violet-700 ring-1 ring-transparent hover:bg-violet-100 hover:ring-violet-100', + destructiveGhost: + 'bg-transparent text-red-700 ring-1 ring-transparent hover:bg-red-100 hover:ring-red-100', + }, + size: { + sm: 'py-1 px-4 text-sm', + md: 'py-2 px-5 text-sm', + lg: 'px-6 py-3 text-lg', + }, + }, + } +); + +const iconVariants = cva('', { + variants: { + size: { + sm: 'h-4 w-4', + md: 'h-5 w-5', + lg: 'h-6 w-6', + }, + }, +}); + +export interface ButtonProps + extends React.ComponentPropsWithoutRef<'button'>, + VariantProps { + isLoading?: boolean; + loadingText?: string; + fullWidth?: boolean; + icon?: React.ReactNode; +} + +const Button = forwardRef((props, ref) => { + const { + isLoading = false, + children, + loadingText, + disabled, + fullWidth = false, + className = '', + variant = 'default', + size = 'md', + icon, + ...rest + } = props; + + const disabledClass = 'opacity-50 cursor-not-allowed pointer-events-none'; + + return ( + + ); +}); + +Button.displayName = 'Button'; +export default Button; diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 0000000..a1f96ed --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; +import { HTMLProps, PropsWithChildren, ReactNode } from 'react'; + +export const Card = ({ + children, + header, + footer, + className, + ...props +}: PropsWithChildren & { + header?: ReactNode; + footer?: ReactNode; +} & HTMLProps) => { + return ( +
+ {header &&
{header}
} +
+ {children} +
+ {footer &&
{footer}
} +
+ ); +}; + +export default Card; diff --git a/src/components/CounterTag.tsx b/src/components/CounterTag.tsx new file mode 100644 index 0000000..a612143 --- /dev/null +++ b/src/components/CounterTag.tsx @@ -0,0 +1,22 @@ +import { HTMLProps } from 'react'; + +export const CounterTag = ({ + count, + className, + ...props +}: HTMLProps & { count: number }) => { + return ( + <> + {count > 0 && ( + + {count} + + )} + + ); +}; diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx new file mode 100644 index 0000000..b451955 --- /dev/null +++ b/src/components/Dialog.tsx @@ -0,0 +1,78 @@ +import React, { FC } from 'react'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { XMarkIcon } from '@heroicons/react/24/solid'; +import clsx from 'clsx'; + +export const Dialog: FC = ({ children, ...props }) => ( + {children} +); + +interface DialogContentProps extends RadixDialog.DialogContentProps { + title?: string; + description?: string; + children?: React.ReactNode; + className?: string; + containerClassName?: string; + closeIcon?: boolean; +} + +export const DialogContent = React.forwardRef< + HTMLDivElement, + DialogContentProps +>(({ children, className = '', containerClassName, ...props }, ref) => ( + + + +
+
+
+
+
+ {props.title && ( + + {props.title} + + )} + {props.closeIcon && ( + + + + )} +
+ {props.description && ( + + {props.description} + + )} +
+
+ {children && ( +
+ {children} +
+ )} +
+
+
+
+)); + +DialogContent.displayName = 'DialogContent'; + +export const DialogTrigger = RadixDialog.Trigger; diff --git a/src/components/Droppable.tsx b/src/components/Droppable.tsx new file mode 100644 index 0000000..a925e36 --- /dev/null +++ b/src/components/Droppable.tsx @@ -0,0 +1,6 @@ +import dynamic from 'next/dynamic'; + +export default dynamic( + () => import('react-beautiful-dnd').then((res) => res.Droppable), + { ssr: false } +); diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..e304395 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,63 @@ +import clsx from 'clsx'; +import { forwardRef, HTMLProps } from 'react'; + +export const Input = forwardRef>( + function Input({ className, ...props }, ref) { + return ( + + ); + } +); + +export const TextArea = forwardRef< + HTMLTextAreaElement, + HTMLProps +>(function TextArea({ className, ...props }, ref) { + return ( +