From 8e8f142eadb3801ef2b85476fc3e88419489b7d3 Mon Sep 17 00:00:00 2001 From: Aleksandar Ivanov Date: Wed, 27 Mar 2024 15:14:57 +0100 Subject: [PATCH 1/8] updated tiptap dependencies + added text align --- apps/web/package.json | 11 +- pnpm-lock.yaml | 673 ++++++++++++++---------------------------- 2 files changed, 223 insertions(+), 461 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 5757326d..f7440fdb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -60,11 +60,12 @@ "@react-email/text": "0.0.3", "@stripe/stripe-js": "^1.52.1", "@tanstack/react-query": "^4.29.1", - "@tiptap/extension-bubble-menu": "2.1.13", - "@tiptap/extension-placeholder": "2.1.13", - "@tiptap/pm": "2.1.13", - "@tiptap/react": "2.1.13", - "@tiptap/starter-kit": "2.1.13", + "@tiptap/extension-bubble-menu": "2.2.4", + "@tiptap/extension-placeholder": "2.2.4", + "@tiptap/extension-text-align": "2.2.4", + "@tiptap/pm": "2.2.4", + "@tiptap/react": "2.2.4", + "@tiptap/starter-kit": "2.2.4", "@trpc/client": "^10.19.1", "@trpc/next": "^10.19.1", "@trpc/react-query": "^10.19.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55b642d0..8ef4f624 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - importers: .: @@ -16,10 +12,10 @@ importers: version: link:packages/eslint-config-custom prettier: specifier: latest - version: 3.1.1 + version: 3.2.5 turbo: specifier: latest - version: 1.11.2 + version: 1.12.5 apps/angular-example: dependencies: @@ -159,7 +155,7 @@ importers: version: 13.0.6(eslint@8.29.0)(typescript@4.9.3) next: specifier: 14.0.4 - version: 14.0.4(react-dom@18.2.0)(react@18.2.0) + version: 14.0.4(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0) next-plausible: specifier: ^3.11.3 version: 3.11.3(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) @@ -315,20 +311,26 @@ importers: specifier: ^4.29.1 version: 4.29.12(react-dom@18.2.0)(react@18.2.0) '@tiptap/extension-bubble-menu': - specifier: 2.1.13 - version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + specifier: 2.2.4 + version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-highlight': + specifier: 2.2.4 + version: 2.2.4(@tiptap/core@2.2.4) '@tiptap/extension-placeholder': - specifier: 2.1.13 - version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) + specifier: 2.2.4 + version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-text-align': + specifier: 2.2.4 + version: 2.2.4(@tiptap/core@2.2.4) '@tiptap/pm': - specifier: 2.1.13 - version: 2.1.13 + specifier: 2.2.4 + version: 2.2.4 '@tiptap/react': - specifier: 2.1.13 - version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(react-dom@18.2.0)(react@18.2.0) + specifier: 2.2.4 + version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(react-dom@18.2.0)(react@18.2.0) '@tiptap/starter-kit': - specifier: 2.1.13 - version: 2.1.13(@tiptap/pm@2.1.13) + specifier: 2.2.4 + version: 2.2.4(@tiptap/pm@2.2.4) '@trpc/client': specifier: ^10.19.1 version: 10.30.0(@trpc/server@10.30.0) @@ -773,7 +775,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(typescript@4.9.5) + version: 6.7.0(ts-node@10.9.1)(typescript@5.3.3) typescript: specifier: ^4.9.3 version: 4.9.5 @@ -876,7 +878,7 @@ importers: version: 8.8.0(eslint@7.32.0) eslint-config-turbo: specifier: latest - version: 1.11.2(eslint@7.32.0) + version: 1.12.5(eslint@7.32.0) eslint-plugin-react: specifier: 7.31.8 version: 7.31.8(eslint@7.32.0) @@ -944,7 +946,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(typescript@4.9.5) + version: 6.7.0(ts-node@10.9.1)(typescript@5.3.3) typescript: specifier: ^4.9.3 version: 4.9.5 @@ -993,7 +995,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(typescript@4.9.5) + version: 6.7.0(ts-node@10.9.1)(typescript@5.3.3) typescript: specifier: ^4.9.3 version: 4.9.5 @@ -1060,7 +1062,7 @@ importers: version: link:../tsconfig tsup: specifier: ^6.5.0 - version: 6.7.0(typescript@4.9.5) + version: 6.7.0(ts-node@10.9.1)(typescript@5.3.3) typescript: specifier: ^4.9.3 version: 4.9.5 @@ -1446,7 +1448,7 @@ packages: tree-kill: 1.2.2 tslib: 2.5.0 typescript: 4.9.5 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) webpack-dev-middleware: 6.0.1(webpack@5.76.1) webpack-dev-server: 4.11.1(webpack@5.76.1) webpack-merge: 5.8.0 @@ -1477,7 +1479,7 @@ packages: dependencies: '@angular-devkit/architect': 0.1502.8(chokidar@3.5.3) rxjs: 6.6.7 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) webpack-dev-server: 4.11.1(webpack@5.76.1) transitivePeerDependencies: - chokidar @@ -1711,7 +1713,6 @@ packages: resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} dependencies: '@babel/highlight': 7.22.5 - dev: false /@babel/code-frame@7.22.5: resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} @@ -1826,6 +1827,7 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true /@babel/core@7.22.9: resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} @@ -1961,6 +1963,7 @@ packages: browserslist: 4.21.7 lru-cache: 5.1.1 semver: 6.3.1 + dev: true /@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9): resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==} @@ -6189,7 +6192,6 @@ packages: strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - dev: false /@eslint/eslintrc@1.4.1: resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} @@ -6320,7 +6322,6 @@ packages: minimatch: 3.1.2 transitivePeerDependencies: - supports-color - dev: false /@humanwhocodes/config-array@0.9.5: resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==} @@ -7064,7 +7065,7 @@ packages: dependencies: '@angular/compiler-cli': 15.2.9(@angular/compiler@15.2.9)(typescript@4.9.5) typescript: 4.9.5 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /@nguniversal/builders@15.2.1(@angular-devkit/build-angular@15.2.8)(@angular/common@15.2.9)(@angular/core@15.2.9)(@types/express@4.17.17)(typescript@4.9.5): resolution: {integrity: sha512-6UbnJlgFv0KcEmYtw8lJ4B9DVpQcTyQd7vEpuvlOJ7RtcKwY+yUbmsEFnUusxM7y6NgZqiFDziAdJ796agySTQ==} @@ -9974,202 +9975,218 @@ packages: unist-util-visit: 5.0.0 dev: false - /@tiptap/core@2.1.13(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-cMC8bgTN63dj1Mv82iDeeLl6sa9kY0Pug8LSalxVEptRmyFVsVxGgu2/6Y3T+9aCYScxfS06EkA8SdzFMAwYTQ==} + /@tiptap/core@2.2.4(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-cRrI8IlLIhCE1hacBQzXIC8dsRvGq6a4lYWQK/BaHuZg21CG7szp3Vd8Ix+ra1f5v0xPOT+Hy+QFNQooRMKMCw==} peerDependencies: '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/pm': 2.1.13 + '@tiptap/pm': 2.2.4 dev: false - /@tiptap/extension-blockquote@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-oe6wSQACmODugoP9XH3Ouffjy4BsOBWfTC+dETHNCG6ZED6ShHN3CB9Vr7EwwRgmm2WLaKAjMO1sVumwH+Z1rg==} + /@tiptap/extension-blockquote@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-FrfPnn0VgVrUwWLwja1afX99JGLp6PE9ThVcmri+tLwUZQvTTVcCvHoCdOakav3/nge1+aV4iE3tQdyq1tWI9Q==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-bold@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-6cHsQTh/rUiG4jkbJer3vk7g60I5tBwEBSGpdxmEHh83RsvevD8+n92PjA24hYYte5RNlATB011E1wu8PVhSvw==} + /@tiptap/extension-bold@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-v3tTLc8YESFZPOGj5ByFr8VbmQ/PTo49T1vsK50VubxIN/5r9cXlKH8kb3dZlZxCxJa3FrXNO/M8rdGBSWQvSg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-bubble-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-Hm7e1GX3AI6lfaUmr6WqsS9MMyXIzCkhh+VQi6K8jj4Q4s8kY4KPoAyD/c3v9pZ/dieUtm2TfqrOCkbHzsJQBg==} + /@tiptap/extension-bubble-menu@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-Nx1fS9jcFlhxaTDYlnayz2UulhK6CMaePc36+7PQIVI+u20RhgTCRNr25zKNemvsiM0RPZZVUjlHkxC0l5as1Q==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 tippy.js: 6.3.7 dev: false - /@tiptap/extension-bullet-list@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-NkWlQ5bLPUlcROj6G/d4oqAxMf3j3wfndGOPp0z8OoXJtVbVoXl/aMSlLbVgE6n8r6CS8MYxKhXNxrb7Ll2foA==} + /@tiptap/extension-bullet-list@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-z/MPmW8bhRougMuorl6MAQBXeK4rhlP+jBWlNwT+CT8h5IkXqPnDbM1sZeagp2nYfVV6Yc4RWpzimqHHtGnYTA==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-code-block@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-E3tweNExPOV+t1ODKX0MDVsS0aeHGWc1ECt+uyp6XwzsN0bdF2A5+pttQqM7sTcMnQkVACGFbn9wDeLRRcfyQg==} + /@tiptap/extension-code-block@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-h6WV9TmaBEZmvqe1ezMR83DhCPUap6P2mSR5pwVk0WVq6rvZjfgU0iF3EetBJOeDgPlz7cNe2NMDfVb1nGTM/g==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 dev: false - /@tiptap/extension-code@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w==} + /@tiptap/extension-code@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-JB4SJ2mUU/9qXFUf+K5K9szvovnN9AIcCb0f0UlcVBuddKHSqCl3wO3QJgYt44BfQTLMNuyzr+zVqfFd6BNt/g==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-document@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-wLwiTWsVmZTGIE5duTcHRmW4ulVxNW4nmgfpk95+mPn1iKyNGtrVhGWleLhBlTj+DWXDtcfNWZgqZkZNzhkqYQ==} + /@tiptap/extension-document@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-z+05xGK0OFoXV1GL+/8bzcZuWMdMA3+EKwk5c+iziG60VZcvGTF7jBRsZidlu9Oaj0cDwWHCeeo6L9SgSh6i2A==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-dropcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-NAyJi4BJxH7vl/2LNS1X0ndwFKjEtX+cRgshXCnMyh7qNpIRW6Plczapc/W1OiMncOEhZJfpZfkRSfwG01FWFg==} + /@tiptap/extension-dropcursor@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-IHwkEKmqpqXyJi16h7871NrcIqeyN7I6XRE2qdqi+MhGigVWI8nWHoYbjRKa7K/1uhs5zeRYyDlq5EuZyL6mgA==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 dev: false - /@tiptap/extension-floating-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-9Oz7pk1Nts2+EyY+rYfnREGbLzQ5UFazAvRhF6zAJdvyuDmAYm0Jp6s0GoTrpV0/dJEISoFaNpPdMJOb9EBNRw==} + /@tiptap/extension-floating-menu@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-U25l7PEzOmlAPugNRl8t8lqyhQZS6W/+3f92+FdwW9qXju3i62iX/3OGCC3Gv+vybmQ4fbZmMjvl+VDfenNi3A==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 tippy.js: 6.3.7 dev: false - /@tiptap/extension-gapcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-Cl5apsoTcyPPCgE3ThufxQxZ1wyqqh+9uxUN9VF9AbeTkid6oPZvKXwaILf6AFnkSy+SuKrb9kZD2iaezxpzXw==} + /@tiptap/extension-gapcursor@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-Y6htT/RDSqkQ1UwG2Ia+rNVRvxrKPOs3RbqKHPaWr3vbFWwhHyKhMCvi/FqfI3d5pViVHOZQ7jhb5hT/a0BmNw==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 dev: false - /@tiptap/extension-hard-break@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-TGkMzMQayuKg+vN4du0x1ahEItBLcCT1jdWeRsjdM8gHfzbPLdo4PQhVsvm1I0xaZmbJZelhnVsUwRZcIu1WNA==} + /@tiptap/extension-hard-break@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-FPvS57GcqHIeLbPKGJa3gnH30Xw+YB1PXXnAWG2MpnMtc2Vtj1l5xaYYBZB+ADdXLAlU0YMbKhFLQO4+pg1Isg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-heading@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-PEmc19QLmlVUTiHWoF0hpgNTNPNU0nlaFmMKskzO+cx5Df4xvHmv/UqoIwp7/UFbPMkfVJT1ozQU7oD1IWn9Hg==} + /@tiptap/extension-heading@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-gkq7Ns2FcrOCRq7Q+VRYt5saMt2R9g4REAtWy/jEevJ5UV5vA2AiGnYDmxwAkHutoYU0sAUkjqx37wE0wpamNw==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-history@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-1ouitThGTBUObqw250aDwGLMNESBH5PRXIGybsCFO1bktdmWtEw7m72WY41EuX2BH8iKJpcYPerl3HfY1vmCNw==} + /@tiptap/extension-highlight@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-GGl6ehKQ0Q0gGgUQhkWg2XYPfhVU5c0JD3NHzV4OrBP6JAtFeMYeSLdfYzFcmoYnGafvSZaJ3NukUvnDHZGzRg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + dev: false + + /@tiptap/extension-history@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-FDM32XYF5NU4mzh+fJ8w2CyUqv0l2Nl15sd6fOhQkVxSj8t57z+DUXc9ZR3zkH+1RAagYJo/2Gu3e99KpMr0tg==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 dev: false - /@tiptap/extension-horizontal-rule@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-7OgjgNqZXvBejgULNdMSma2M1nzv4bbZG+FT5XMFZmEOxR9IB1x/RzChjPdeicff2ZK2sfhMBc4Y9femF5XkUg==} + /@tiptap/extension-horizontal-rule@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-iCRHjFQQHApWg3R4fkKkJQhWEOdu1Fdc4YEAukdOXPSg3fg36IwjvsMXjt9SYBtVZ+iio3rORCZGXyMvgCH9uw==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 dev: false - /@tiptap/extension-italic@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==} + /@tiptap/extension-italic@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-qIhGNvWnsQswSgEMRA8jQQjxfkOGNAuNWKEVQX9DPoqAUgknT41hQcAMP8L2+OdACpb2jbVMOO5Cy5Dof2L8/w==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-list-item@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-6e8iiCWXOiJTl1XOwVW2tc0YG18h70HUtEHFCx2m5HspOGFKsFEaSS3qYxOheM9HxlmQeDt8mTtqftRjEFRxPQ==} + /@tiptap/extension-list-item@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-lPLKGKsHpM9ClUa8n7GEUn8pG6HCYU0vFruIy3l2t6jZdHkrgBnYtVGMZ13K8UDnj/hlAlccxku0D0P4mA1Vrg==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-ordered-list@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-UO4ZAL5Vrr1WwER5VjgmeNIWHpqy9cnIRo1En07gZ0OWTjs1eITPcu+4TCn1ZG6DhoFvAQzE5DTxxdhIotg+qw==} + /@tiptap/extension-ordered-list@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-TpFy140O9Af1JciXt+xwqYUXxcJ6YG8zi/B5UDJujp+FH5sCmlYYBBnWxiFMhVaj6yEmA2eafu1qUkic/1X5Aw==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-paragraph@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-cEoZBJrsQn69FPpUMePXG/ltGXtqKISgypj70PEHXt5meKDjpmMVSY4/8cXvFYEYsI9GvIwyAK0OrfAHiSoROA==} + /@tiptap/extension-paragraph@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-m1KwyvTNJxsq7StbspbcOhxO4Wk4YpElDbqOouWi+H4c8azdpI5Pn96ZqhFeE9bSyjByg6OcB/wqoJsLbeFWdQ==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-placeholder@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-vIY7y7UbqsrAW/y8bDE9eRenbQEU16kNHB5Wri8RU1YiUZpkPgdXP/pLqyjIIq95SwP/vdTIHjHoQ77VLRl1hA==} + /@tiptap/extension-placeholder@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-UL4Fn9T33SoS7vdI3NnSxBJVeGUIgCIutgXZZ5J8CkcRoDIeS78z492z+6J+qGctHwTd0xUL5NzNJI82HfiTdg==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 + dev: false + + /@tiptap/extension-strike@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-/a2EwQgA+PpG17V2tVRspcrIY0SN3blwcgM7lxdW4aucGkqSKnf7+91dkhQEwCZ//o8kv9mBCyRoCUcGy6S5Xg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + dependencies: + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-strike@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-VN6zlaCNCbyJUCDyBFxavw19XmQ4LkCh8n20M8huNqW77lDGXA2A7UcWLHaNBpqAijBRu9mWI8l4Bftyf2fcAw==} + /@tiptap/extension-text-align@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-iojhpsv3n/r4g/4wMFl1d85RloWrAV3TRUJluurPQZJdrJ7ynJ2fiPqmigAXyaYAJ3+a1ryu9JPlktT9RdYO/A==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-text@2.1.13(@tiptap/core@2.1.13): - resolution: {integrity: sha512-zzsTTvu5U67a8WjImi6DrmpX2Q/onLSaj+LRWPh36A1Pz2WaxW5asZgaS+xWCnR+UrozlCALWa01r7uv69jq0w==} + /@tiptap/extension-text@2.2.4(@tiptap/core@2.2.4): + resolution: {integrity: sha512-NlKHMPnRJXB+0AGtDlU0P2Pg+SdesA2lMMd7JzDUgJgL7pX2jOb8eUqSeOjFKuSzFSqYfH6C3o6mQiNhuQMv+g==} peerDependencies: '@tiptap/core': ^2.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/pm@2.1.13: - resolution: {integrity: sha512-zNbA7muWsHuVg12GrTgN/j119rLePPq5M8dZgkKxUwdw8VmU3eUyBp1SihPEXJ2U0MGdZhNhFX7Y74g11u66sg==} + /@tiptap/pm@2.2.4: + resolution: {integrity: sha512-Po0klR165zgtinhVp1nwMubjyKx6gAY9kH3IzcniYLCkqhPgiqnAcCr61TBpp4hfK8YURBS4ihvCB1dyfCyY8A==} dependencies: prosemirror-changeset: 2.2.1 prosemirror-collab: 1.3.1 @@ -10186,49 +10203,49 @@ packages: prosemirror-schema-list: 1.3.0 prosemirror-state: 1.4.3 prosemirror-tables: 1.3.5 - prosemirror-trailing-node: 2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.6) + prosemirror-trailing-node: 2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.33.3) prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.6 + prosemirror-view: 1.33.3 dev: false - /@tiptap/react@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Dq3f8EtJnpImP3iDtJo+7bulnN9SJZRZcVVzxHXccLcC2MxtmDdlPGZjP+wxO800nd8toSIOd5734fPNf/YcfA==} + /@tiptap/react@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HkYmMZWcETPZn3KpzdDg/ns2TKeFh54TvtCEInA4ljYtWGLoZc/A+KaiEtMIgVs+Mo1XwrhuoNGjL9c0OK2HJw==} peerDependencies: '@tiptap/core': ^2.0.0 '@tiptap/pm': ^2.0.0 react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/extension-bubble-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) - '@tiptap/extension-floating-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) - '@tiptap/pm': 2.1.13 + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/extension-bubble-menu': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-floating-menu': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/pm': 2.2.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@tiptap/starter-kit@2.1.13(@tiptap/pm@2.1.13): - resolution: {integrity: sha512-ph/mUR/OwPtPkZ5rNHINxubpABn8fHnvJSdhXFrY/q6SKoaO11NZXgegRaiG4aL7O6Sz4LsZVw6Sm0Ae+GJmrg==} - dependencies: - '@tiptap/core': 2.1.13(@tiptap/pm@2.1.13) - '@tiptap/extension-blockquote': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-bold': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-bullet-list': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-code': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-code-block': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) - '@tiptap/extension-document': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-dropcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) - '@tiptap/extension-gapcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) - '@tiptap/extension-hard-break': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-heading': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-history': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) - '@tiptap/extension-horizontal-rule': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13) - '@tiptap/extension-italic': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-list-item': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-ordered-list': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-paragraph': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-strike': 2.1.13(@tiptap/core@2.1.13) - '@tiptap/extension-text': 2.1.13(@tiptap/core@2.1.13) + /@tiptap/starter-kit@2.2.4(@tiptap/pm@2.2.4): + resolution: {integrity: sha512-Kbk7qUfIZg3+bNa3e/wBeDQt4jJB46uQgM+xy5NSY6H8NZP6gdmmap3aIrn9S/W/hGpxJl4RcXAeaT0CQji9XA==} + dependencies: + '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) + '@tiptap/extension-blockquote': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-bold': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-bullet-list': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-code': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-code-block': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-document': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-dropcursor': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-gapcursor': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-hard-break': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-heading': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-history': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-horizontal-rule': 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) + '@tiptap/extension-italic': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-list-item': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-ordered-list': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-paragraph': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-strike': 2.2.4(@tiptap/core@2.2.4) + '@tiptap/extension-text': 2.2.4(@tiptap/core@2.2.4) transitivePeerDependencies: - '@tiptap/pm' dev: false @@ -10494,24 +10511,12 @@ packages: resolution: {integrity: sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==} dev: true - /@types/eslint-scope@3.7.4: - resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} - dependencies: - '@types/eslint': 8.40.1 - '@types/estree': 1.0.1 - /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: '@types/eslint': 8.44.9 '@types/estree': 1.0.5 - /@types/eslint@8.40.1: - resolution: {integrity: sha512-vRb792M4mF1FBT+eoLecmkpLXwxsBHvWWRGJjzbYANBM6DtiJc6yETyv4rqDA6QNjF1pkj1U7LMA6dGb3VYlHw==} - dependencies: - '@types/estree': 1.0.1 - '@types/json-schema': 7.0.12 - /@types/eslint@8.44.9: resolution: {integrity: sha512-6yBxcvwnnYoYT1Uk2d+jvIfsuP4mb2EdIxFnrPABj5a/838qe5bGkNLFOiipX4ULQ7XVQvTxOh7jO+BTAiqsEw==} dependencies: @@ -11025,7 +11030,6 @@ packages: typescript: 4.9.5 transitivePeerDependencies: - supports-color - dev: false /@typescript-eslint/parser@5.59.9(eslint@8.29.0)(typescript@4.9.3): resolution: {integrity: sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==} @@ -11571,7 +11575,6 @@ packages: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: acorn: 7.4.1 - dev: false /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -11600,7 +11603,6 @@ packages: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} hasBin: true - dev: false /acorn@8.10.0: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} @@ -11930,7 +11932,6 @@ packages: /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - dev: false /astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} @@ -12045,7 +12046,7 @@ packages: '@babel/core': 7.20.12 find-cache-dir: 3.3.2 schema-utils: 4.1.0 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} @@ -13162,7 +13163,7 @@ packages: normalize-path: 3.0.0 schema-utils: 4.1.0 serialize-javascript: 6.0.1 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /core-js-compat@3.30.2: resolution: {integrity: sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==} @@ -13252,7 +13253,7 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.31) postcss-value-parser: 4.2.0 semver: 7.5.1 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -14237,6 +14238,7 @@ packages: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 + dev: false /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} @@ -14567,7 +14569,7 @@ packages: eslint: 8.42.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5)(eslint@8.42.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@2.7.1)(eslint@8.42.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.42.0) eslint-plugin-react: 7.32.2(eslint@8.42.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.42.0) @@ -14592,7 +14594,7 @@ packages: eslint: 8.29.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.29.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.29.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.29.0) eslint-plugin-react: 7.32.2(eslint@8.29.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.29.0) @@ -14611,13 +14613,13 @@ packages: eslint: 7.32.0 dev: false - /eslint-config-turbo@1.11.2(eslint@7.32.0): - resolution: {integrity: sha512-vqbyCH6kCHFoIAWUmGL61c0BfUQNz0XAl2RzAnEkSQ+PLXvEvuV2HsvL51UOzyyElfJlzZuh9T4BvUqb5KR9Eg==} + /eslint-config-turbo@1.12.5(eslint@7.32.0): + resolution: {integrity: sha512-wXytbX+vTzQ6rwgM6sIr447tjYJBlRj5V/eBFNGNXw5Xs1R715ppPYhbmxaFbkrWNQSGJsWRrYGAlyq0sT/OsQ==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 7.32.0 - eslint-plugin-turbo: 1.11.2(eslint@7.32.0) + eslint-plugin-turbo: 1.12.5(eslint@7.32.0) dev: false /eslint-import-resolver-node@0.3.7: @@ -14645,7 +14647,6 @@ packages: tsconfig-paths: 3.14.2 transitivePeerDependencies: - supports-color - dev: false /eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5)(eslint@8.42.0): resolution: {integrity: sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==} @@ -14656,7 +14657,7 @@ packages: dependencies: debug: 4.3.4 eslint: 8.42.0 - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@2.7.1)(eslint@8.42.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) glob: 7.2.3 is-glob: 4.0.3 resolve: 1.22.2 @@ -14676,7 +14677,7 @@ packages: enhanced-resolve: 5.14.1 eslint: 8.29.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.29.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.29.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) get-tsconfig: 4.6.0 globby: 13.1.4 is-core-module: 2.12.1 @@ -14717,37 +14718,6 @@ packages: eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5)(eslint@7.32.0) transitivePeerDependencies: - supports-color - dev: false - - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1)(eslint@8.42.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - '@typescript-eslint/parser': 5.59.9(eslint@8.42.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@5.5.0) - eslint: 8.42.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5)(eslint@8.42.0) - transitivePeerDependencies: - - supports-color - dev: true /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.29.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} @@ -14810,73 +14780,6 @@ packages: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - dev: false - - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@2.7.1)(eslint@8.42.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - '@typescript-eslint/parser': 5.59.9(eslint@8.42.0)(typescript@4.9.5) - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@5.5.0) - doctrine: 2.1.0 - eslint: 8.42.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1)(eslint@8.42.0) - has: 1.0.3 - is-core-module: 2.12.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.2 - semver: 6.3.1 - tsconfig-paths: 3.14.2 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.29.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - '@typescript-eslint/parser': 5.59.9(eslint@8.29.0)(typescript@4.9.3) - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@5.5.0) - doctrine: 2.1.0 - eslint: 8.29.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.29.0) - has: 1.0.3 - is-core-module: 2.12.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.2 - semver: 6.3.1 - tsconfig-paths: 3.14.2 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: false /eslint-plugin-jsx-a11y@6.7.1(eslint@7.32.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} @@ -15051,8 +14954,8 @@ packages: string.prototype.matchall: 4.0.8 dev: true - /eslint-plugin-turbo@1.11.2(eslint@7.32.0): - resolution: {integrity: sha512-U6DX+WvgGFiwEAqtOjm4Ejd9O4jsw8jlFNkQi0ywxbMnbiTie+exF4Z0F/B1ajtjjeZkBkgRnlU+UkoraBN+bw==} + /eslint-plugin-turbo@1.12.5(eslint@7.32.0): + resolution: {integrity: sha512-cXy7mCzAdngBTJIWH4DASXHy0vQpujWDBqRTu0YYqCN/QEGsi3HWM+STZEbPYELdjtm5EsN2HshOSSqWnjdRHg==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -15079,7 +14982,6 @@ packages: engines: {node: '>=6'} dependencies: eslint-visitor-keys: 1.3.0 - dev: false /eslint-utils@3.0.0(eslint@8.29.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} @@ -15104,7 +15006,6 @@ packages: /eslint-visitor-keys@1.3.0: resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} engines: {node: '>=4'} - dev: false /eslint-visitor-keys@2.1.0: resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} @@ -15161,7 +15062,6 @@ packages: v8-compile-cache: 2.3.0 transitivePeerDependencies: - supports-color - dev: false /eslint@8.29.0: resolution: {integrity: sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==} @@ -15317,7 +15217,6 @@ packages: acorn: 7.4.1 acorn-jsx: 5.3.2(acorn@7.4.1) eslint-visitor-keys: 1.3.0 - dev: false /espree@9.2.0: resolution: {integrity: sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg==} @@ -18262,7 +18161,7 @@ packages: dependencies: klona: 2.0.6 less: 4.1.3 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /less@4.1.3: resolution: {integrity: sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==} @@ -18310,7 +18209,7 @@ packages: webpack-sources: optional: true dependencies: - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) webpack-sources: 3.2.3 /lilconfig@2.1.0: @@ -18444,7 +18343,6 @@ packages: /lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - dev: false /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -19798,7 +19696,7 @@ packages: webpack: ^5.0.0 dependencies: schema-utils: 4.1.0 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} @@ -20201,7 +20099,7 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - next: 14.0.4(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -20237,7 +20135,7 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' dependencies: - next: 14.0.4(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -20351,46 +20249,6 @@ packages: - babel-plugin-macros dev: false - /next@14.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - sass: - optional: true - dependencies: - '@next/env': 14.0.4 - '@swc/helpers': 0.5.2 - busboy: 1.6.0 - caniuse-lite: 1.0.30001582 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.22.5)(react@18.2.0) - watchpack: 2.4.0 - optionalDependencies: - '@next/swc-darwin-arm64': 14.0.4 - '@next/swc-darwin-x64': 14.0.4 - '@next/swc-linux-arm64-gnu': 14.0.4 - '@next/swc-linux-arm64-musl': 14.0.4 - '@next/swc-linux-x64-gnu': 14.0.4 - '@next/swc-linux-x64-musl': 14.0.4 - '@next/swc-win32-arm64-msvc': 14.0.4 - '@next/swc-win32-ia32-msvc': 14.0.4 - '@next/swc-win32-x64-msvc': 14.0.4 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - /nextjs-cors@2.1.2(next@14.0.4): resolution: {integrity: sha512-2yOVivaaf2ILe4f/qY32hnj3oC77VCOsUQJQfhVMGsXE/YMEWUY2zy78sH9FKUCM7eG42/l3pDofIzMD781XGA==} peerDependencies: @@ -20417,7 +20275,7 @@ packages: git-url-parse: 13.1.1 intersection-observer: 0.12.2 match-sorter: 6.3.1 - next: 14.0.4(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0) next-seo: 6.4.0(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) next-themes: 0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) nextra: 2.13.2(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) @@ -20447,7 +20305,7 @@ packages: gray-matter: 4.0.3 katex: 0.16.9 lodash.get: 4.4.2 - next: 14.0.4(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(@babel/core@7.23.6)(react-dom@18.2.0)(react@18.2.0) next-mdx-remote: 4.4.1(react-dom@18.2.0)(react@18.2.0) p-limit: 3.1.0 react: 18.2.0 @@ -21504,7 +21362,7 @@ packages: klona: 2.0.6 postcss: 8.4.21 semver: 7.5.1 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /postcss-modules-extract-imports@3.0.0(postcss@8.4.31): resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} @@ -21686,8 +21544,8 @@ packages: hasBin: true dev: false - /prettier@3.1.1: - resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} engines: {node: '>=14'} hasBin: true dev: true @@ -21824,7 +21682,7 @@ packages: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.6 + prosemirror-view: 1.33.3 dev: false /prosemirror-gapcursor@1.3.2: @@ -21833,7 +21691,7 @@ packages: prosemirror-keymap: 1.2.2 prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 - prosemirror-view: 1.32.6 + prosemirror-view: 1.33.3 dev: false /prosemirror-history@1.3.2: @@ -21841,7 +21699,7 @@ packages: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.6 + prosemirror-view: 1.33.3 rope-sequence: 1.3.4 dev: false @@ -21900,7 +21758,7 @@ packages: dependencies: prosemirror-model: 1.19.4 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.6 + prosemirror-view: 1.33.3 dev: false /prosemirror-tables@1.3.5: @@ -21910,10 +21768,10 @@ packages: prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - prosemirror-view: 1.32.6 + prosemirror-view: 1.33.3 dev: false - /prosemirror-trailing-node@2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.32.6): + /prosemirror-trailing-node@2.0.7(prosemirror-model@1.19.4)(prosemirror-state@1.4.3)(prosemirror-view@1.33.3): resolution: {integrity: sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==} peerDependencies: prosemirror-model: ^1.19.0 @@ -21925,7 +21783,7 @@ packages: escape-string-regexp: 4.0.0 prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 - prosemirror-view: 1.32.6 + prosemirror-view: 1.33.3 dev: false /prosemirror-transform@1.8.0: @@ -21934,8 +21792,8 @@ packages: prosemirror-model: 1.19.4 dev: false - /prosemirror-view@1.32.6: - resolution: {integrity: sha512-26r5LvyDlPgUNVf7ZdNdGrMJnylwjJtUJTfDuYOANIVx9lqWD1WCBlGg283weYQGKUC64DXR25LeAmliB9CrFQ==} + /prosemirror-view@1.33.3: + resolution: {integrity: sha512-P4Ao/bc4OrU/2yLIf8dL4lJaEtjLR3QjIvQHgJYp2jUS7kYM4bSR6okbBjkqzOs/FwUon6UGjTLdKMnPL1MZqw==} dependencies: prosemirror-model: 1.19.4 prosemirror-state: 1.4.3 @@ -22891,7 +22749,7 @@ packages: klona: 2.0.6 neo-async: 2.6.2 sass: 1.58.1 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /sass@1.58.1: resolution: {integrity: sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg==} @@ -23267,7 +23125,6 @@ packages: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - dev: false /smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} @@ -23389,7 +23246,7 @@ packages: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.0.2 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -23777,6 +23634,7 @@ packages: '@babel/core': 7.22.5 client-only: 0.0.1 react: 18.2.0 + dev: true /styled-jsx@5.1.1(@babel/core@7.23.6)(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} @@ -24026,7 +23884,6 @@ packages: slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: false /tailwind-merge@1.13.1: resolution: {integrity: sha512-tRtRN22TDokGi2TuYSvuHQuuW6BJ/zlUEG+iYpAQ9i66msc/0eU/+HPccbPnNNH0mCPp0Ob8thaC8Uy9CxHitQ==} @@ -24162,31 +24019,7 @@ packages: schema-utils: 3.2.0 serialize-javascript: 6.0.1 terser: 5.17.7 - webpack: 5.76.1 - - /terser-webpack-plugin@5.3.9(webpack@5.89.0): - resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - dependencies: - '@jridgewell/trace-mapping': 0.3.20 - jest-worker: 27.5.1 - schema-utils: 3.2.0 - serialize-javascript: 6.0.1 - terser: 5.17.7 - webpack: 5.89.0 - dev: false + webpack: 5.76.1(esbuild@0.17.8) /terser@5.16.3: resolution: {integrity: sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==} @@ -24562,43 +24395,6 @@ packages: transitivePeerDependencies: - supports-color - ts-node - dev: false - - /tsup@6.7.0(typescript@4.9.5): - resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} - engines: {node: '>=14.18'} - hasBin: true - peerDependencies: - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.1.0' - peerDependenciesMeta: - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - dependencies: - bundle-require: 4.0.1(esbuild@0.17.19) - cac: 6.7.14 - chokidar: 3.5.3 - debug: 4.3.4 - esbuild: 0.17.19 - execa: 5.1.1 - globby: 11.1.0 - joycon: 3.1.1 - postcss-load-config: 3.1.4(ts-node@10.9.1) - resolve-from: 5.0.0 - rollup: 3.25.0 - source-map: 0.8.0-beta.0 - sucrase: 3.32.0 - tree-kill: 1.2.2 - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - - ts-node - dev: true /tsutils@3.21.0(typescript@4.9.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -24644,64 +24440,64 @@ packages: - supports-color dev: true - /turbo-darwin-64@1.11.2: - resolution: {integrity: sha512-toFmRG/adriZY3hOps7nYCfqHAS+Ci6xqgX3fbo82kkLpC6OBzcXnleSwuPqjHVAaRNhVoB83L5njcE9Qwi2og==} + /turbo-darwin-64@1.12.5: + resolution: {integrity: sha512-0GZ8reftwNQgIQLHkHjHEXTc/Z1NJm+YjsrBP+qhM/7yIZ3TEy9gJhuogDt2U0xIWwFgisTyzbtU7xNaQydtoA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.11.2: - resolution: {integrity: sha512-FCsEDZ8BUSFYEOSC3rrARQrj7x2VOrmVcfrMUIhexTxproRh4QyMxLfr6LALk4ymx6jbDCxWa6Szal8ckldFbA==} + /turbo-darwin-arm64@1.12.5: + resolution: {integrity: sha512-8WpOLNNzvH6kohQOjihD+gaWL+ZFNfjvBwhOF0rjEzvW+YR3Pa7KjhulrjWyeN2yMFqAPubTbZIGOz1EVXLuQA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.11.2: - resolution: {integrity: sha512-Vzda/o/QyEske5CxLf0wcu7UUS+7zB90GgHZV4tyN+WZtoouTvbwuvZ3V6b5Wgd3OJ/JwWR0CXDK7Sf4VEMr7A==} + /turbo-linux-64@1.12.5: + resolution: {integrity: sha512-INit73+bNUpwqGZCxgXCR3I+cQsdkQ3/LkfkgSOibkpg+oGqxJRzeXw3sp990d7SCoE8QOcs3iw+PtiFX/LDAA==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.11.2: - resolution: {integrity: sha512-bRLwovQRz0yxDZrM4tQEAYV0fBHEaTzUF0JZ8RG1UmZt/CqtpnUrJpYb1VK8hj1z46z9YehARpYCwQ2K0qU4yw==} + /turbo-linux-arm64@1.12.5: + resolution: {integrity: sha512-6lkRBvxtI/GQdGtaAec9LvVQUoRw6nXFp0kM+Eu+5PbZqq7yn6cMkgDJLI08zdeui36yXhone8XGI8pHg8bpUQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.11.2: - resolution: {integrity: sha512-LgTWqkHAKgyVuLYcEPxZVGPInTjjeCnN5KQMdJ4uQZ+xMDROvMFS2rM93iQl4ieDJgidwHCxxCxaU9u8c3d/Kg==} + /turbo-windows-64@1.12.5: + resolution: {integrity: sha512-gQYbOhZg5Ww0bQ/bC0w/4W6yQRwBumUUnkB+QPo15VznwxZe2a7bo6JM+9Xy9dKLa/kn+p7zTqme4OEp6M3/Yg==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.11.2: - resolution: {integrity: sha512-829aVBU7IX0c/B4G7g1VI8KniAGutHhIupkYMgF6xPkYVev2G3MYe6DMS/vsLt9GGM9ulDtdWxWrH5P2ngK8IQ==} + /turbo-windows-arm64@1.12.5: + resolution: {integrity: sha512-auvhZ9FrhnvQ4mgBlY9O68MT4dIfprYGvd2uPICba/mHUZZvVy5SGgbHJ0KbMwaJfnnFoPgLJO6M+3N2gDprKw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.11.2: - resolution: {integrity: sha512-jPC7LVQJzebs5gWf8FmEvsvXGNyKbN+O9qpvv98xpNaM59aS0/Irhd0H0KbcqnXfsz7ETlzOC3R+xFWthC4Z8A==} + /turbo@1.12.5: + resolution: {integrity: sha512-FATU5EnhrYG8RvQJYFJnDd18DpccDjyvd53hggw9T9JEg9BhWtIEoeaKtBjYbpXwOVrJQMDdXcIB4f2nD3QPPg==} hasBin: true optionalDependencies: - turbo-darwin-64: 1.11.2 - turbo-darwin-arm64: 1.11.2 - turbo-linux-64: 1.11.2 - turbo-linux-arm64: 1.11.2 - turbo-windows-64: 1.11.2 - turbo-windows-arm64: 1.11.2 + turbo-darwin-64: 1.12.5 + turbo-darwin-arm64: 1.12.5 + turbo-linux-64: 1.12.5 + turbo-linux-arm64: 1.12.5 + turbo-windows-64: 1.12.5 + turbo-windows-arm64: 1.12.5 dev: true /type-check@0.3.2: @@ -25686,7 +25482,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.1.0 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /webpack-dev-middleware@6.0.1(webpack@5.76.1): resolution: {integrity: sha512-PZPZ6jFinmqVPJZbisfggDiC+2EeGZ1ZByyMP5sOFJcPPWSexalISz+cvm+j+oYPT7FIJyxT76esjnw9DhE5sw==} @@ -25699,7 +25495,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.1.0 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /webpack-dev-server@4.11.1(webpack@5.76.1): resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==} @@ -25739,7 +25535,7 @@ packages: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) webpack-dev-middleware: 5.3.3(webpack@5.76.1) ws: 8.13.0 transitivePeerDependencies: @@ -25778,13 +25574,13 @@ packages: optional: true dependencies: typed-assert: 1.0.9 - webpack: 5.76.1 + webpack: 5.76.1(esbuild@0.17.8) /webpack-virtual-modules@0.4.6: resolution: {integrity: sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==} dev: true - /webpack@5.76.1: + /webpack@5.76.1(esbuild@0.17.8): resolution: {integrity: sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==} engines: {node: '>=10.13.0'} hasBin: true @@ -25823,45 +25619,6 @@ packages: - esbuild - uglify-js - /webpack@5.76.1(esbuild@0.17.8): - resolution: {integrity: sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - dependencies: - '@types/eslint-scope': 3.7.4 - '@types/estree': 0.0.51 - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/wasm-edit': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 - acorn: 8.11.2 - acorn-import-assertions: 1.9.0(acorn@8.11.2) - browserslist: 4.21.5 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.14.1 - es-module-lexer: 0.9.3 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.2.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.9(esbuild@0.17.8)(webpack@5.76.1) - watchpack: 2.4.0 - webpack-sources: 3.2.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - /webpack@5.89.0: resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} engines: {node: '>=10.13.0'} @@ -25893,7 +25650,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.9(webpack@5.89.0) + terser-webpack-plugin: 5.3.9(esbuild@0.17.8)(webpack@5.76.1) watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -26424,3 +26181,7 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false From 6a956295d596284788b45edd95c159af758ff2cb Mon Sep 17 00:00:00 2001 From: Aleksandar Ivanov Date: Wed, 27 Mar 2024 15:17:35 +0100 Subject: [PATCH 2/8] updated editor --- apps/web/src/components/Editor.tsx | 182 +++++++++++++++++++++-------- 1 file changed, 134 insertions(+), 48 deletions(-) diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx index 18f31e82..ab83a3a0 100644 --- a/apps/web/src/components/Editor.tsx +++ b/apps/web/src/components/Editor.tsx @@ -1,4 +1,9 @@ -import { BubbleMenu, EditorContent, useEditor } from "@tiptap/react"; +import TextAlign from "@tiptap/extension-text-align"; +import { + Editor as TipTapEditor, + EditorContent, + useEditor, +} from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import clsx from "clsx"; import { twMerge } from "tailwind-merge"; @@ -9,27 +14,147 @@ type Props = { onUpdate?: (html: string) => void; }; +const getMenuButtonClass = ({ + roundCorners = "none", + isActive = false, +}: { + roundCorners?: "left" | "both" | "right" | "none"; + isActive: boolean; +}) => + twMerge( + "bg-primary px-3 py-1 text-primary-foreground hover:bg-pink-500 hover:text-white", + clsx(isActive && "bg-pink-600 text-white", { + "rounded-l-md": roundCorners === "left", + "rounded-r-md": roundCorners === "right", + "rounded-md": roundCorners === "both", + "": roundCorners === "none", + }) + ); + +const MenuBar = ({ editor }: { editor: TipTapEditor | null }) => { + if (!editor) { + return null; + } + + return ( +
+ + + + + + + + + + + +
+ ); +}; + export const Editor = ({ className, content, onUpdate }: Props) => { const editor = useEditor({ extensions: [ StarterKit.configure({ - heading: false, orderedList: { HTMLAttributes: { - class: "list-decimal list-outside ml-4", + class: "list-decimal list-outside", }, }, bulletList: { HTMLAttributes: { - class: "list-disc list-outside ml-4", + class: "list-disc list-outside", }, }, }), + TextAlign.configure({ + types: ["heading", "paragraph"], + }), ], editorProps: { attributes: { class: - "prose-invert prose-base my-5 focus:outline-none border-pink-50/60 border rounded-lg py-3 px-2", + "prose-invert prose-base leading-none my-5 focus:outline-none border-pink-50/60 border rounded-lg py-3 px-2 h-64 overflow-y-auto", }, }, content, @@ -38,51 +163,12 @@ export const Editor = ({ className, content, onUpdate }: Props) => { }, }); - const getBubbleMenuButtonClass = ({ - type, - position = "center", - }: { - type: string; - position: "left" | "center" | "right"; - }) => - twMerge( - "bg-pink-600 px-3 py-1 text-white hover:bg-pink-500", - clsx(editor?.isActive(type) && "bg-pink-500 hover:bg-pink-600", { - "rounded-l-md": position === "left", - "rounded-r-md": position === "right", - "rounded-md": position === "center", - }) - ); - return ( <> - {editor && ( - - - - - )} - + +
+ +
); }; From 3f170e7f15d7f93ca7812b670782ebf81f351dea Mon Sep 17 00:00:00 2001 From: Aleksandar Ivanov Date: Wed, 27 Mar 2024 15:17:51 +0100 Subject: [PATCH 3/8] remove markdown supported message (unnecessary with the menu bar) --- apps/web/src/components/FlagPage.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/web/src/components/FlagPage.tsx b/apps/web/src/components/FlagPage.tsx index c2b61611..b4d45c84 100644 --- a/apps/web/src/components/FlagPage.tsx +++ b/apps/web/src/components/FlagPage.tsx @@ -106,9 +106,6 @@ const EditDescriptionModal = ({ content={newDescription} onUpdate={setNewDescription} /> - - __Markdown__ is **supported** - ); }; From a5b5fe391b9c67f5f90cea298e8a12fa57a380ae Mon Sep 17 00:00:00 2001 From: Simon O <60504110+simonorzel26@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:35:31 +0200 Subject: [PATCH 4/8] feat: adds lucide icons --- apps/web/src/components/Editor.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx index ab83a3a0..de100b97 100644 --- a/apps/web/src/components/Editor.tsx +++ b/apps/web/src/components/Editor.tsx @@ -6,6 +6,7 @@ import { } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import clsx from "clsx"; +import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Heading1, Heading2, Heading3, Italic, Strikethrough } from "lucide-react"; import { twMerge } from "tailwind-merge"; type Props = { @@ -45,7 +46,7 @@ const MenuBar = ({ editor }: { editor: TipTapEditor | null }) => { isActive: editor.isActive("heading", { level: 1 }), })} > - H1 + ); From aa3d9e3b2790602115ddde65073ac8825ab2f9cd Mon Sep 17 00:00:00 2001 From: Simon O <60504110+simonorzel26@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:36:36 +0200 Subject: [PATCH 5/8] feat: pnpm lock --- pnpm-lock.yaml | 53 ++++++++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ef4f624..68e98d3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: version: 3.2.5 turbo: specifier: latest - version: 1.12.5 + version: 1.13.0 apps/angular-example: dependencies: @@ -313,9 +313,6 @@ importers: '@tiptap/extension-bubble-menu': specifier: 2.2.4 version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) - '@tiptap/extension-highlight': - specifier: 2.2.4 - version: 2.2.4(@tiptap/core@2.2.4) '@tiptap/extension-placeholder': specifier: 2.2.4 version: 2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4) @@ -10091,14 +10088,6 @@ packages: '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) dev: false - /@tiptap/extension-highlight@2.2.4(@tiptap/core@2.2.4): - resolution: {integrity: sha512-GGl6ehKQ0Q0gGgUQhkWg2XYPfhVU5c0JD3NHzV4OrBP6JAtFeMYeSLdfYzFcmoYnGafvSZaJ3NukUvnDHZGzRg==} - peerDependencies: - '@tiptap/core': ^2.0.0 - dependencies: - '@tiptap/core': 2.2.4(@tiptap/pm@2.2.4) - dev: false - /@tiptap/extension-history@2.2.4(@tiptap/core@2.2.4)(@tiptap/pm@2.2.4): resolution: {integrity: sha512-FDM32XYF5NU4mzh+fJ8w2CyUqv0l2Nl15sd6fOhQkVxSj8t57z+DUXc9ZR3zkH+1RAagYJo/2Gu3e99KpMr0tg==} peerDependencies: @@ -24440,64 +24429,64 @@ packages: - supports-color dev: true - /turbo-darwin-64@1.12.5: - resolution: {integrity: sha512-0GZ8reftwNQgIQLHkHjHEXTc/Z1NJm+YjsrBP+qhM/7yIZ3TEy9gJhuogDt2U0xIWwFgisTyzbtU7xNaQydtoA==} + /turbo-darwin-64@1.13.0: + resolution: {integrity: sha512-ctHeJXtQgBcgxnCXwrJTGiq57HtwF7zWz5NTuSv//5yeU01BtQIt62ArKfjudOhRefWJbX3Z5srn88XTb9hfww==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.12.5: - resolution: {integrity: sha512-8WpOLNNzvH6kohQOjihD+gaWL+ZFNfjvBwhOF0rjEzvW+YR3Pa7KjhulrjWyeN2yMFqAPubTbZIGOz1EVXLuQA==} + /turbo-darwin-arm64@1.13.0: + resolution: {integrity: sha512-/Q9/pNFkF9w83tNxwMpgapwLYdQ12p8mpty2YQRoUiS9ClWkcqe136jR0mtuMqzlNlpREOFZaoyIthjt6Sdo0g==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.12.5: - resolution: {integrity: sha512-INit73+bNUpwqGZCxgXCR3I+cQsdkQ3/LkfkgSOibkpg+oGqxJRzeXw3sp990d7SCoE8QOcs3iw+PtiFX/LDAA==} + /turbo-linux-64@1.13.0: + resolution: {integrity: sha512-hgbT7o020BGV4L7Sd8hhFTd5zVKPKxbsr0dPfel/9NkdTmptz2aGZ0Vb2MAa18SY3XaCQpDxmdYuOzvvRpo5ZA==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.12.5: - resolution: {integrity: sha512-6lkRBvxtI/GQdGtaAec9LvVQUoRw6nXFp0kM+Eu+5PbZqq7yn6cMkgDJLI08zdeui36yXhone8XGI8pHg8bpUQ==} + /turbo-linux-arm64@1.13.0: + resolution: {integrity: sha512-WK01i2wDZARrV+HEs495A3hNeGMwQR5suYk7G+ceqqW7b+dOTlQdvUjnI3sg7wAnZPgjafFs/hoBaZdJjVa/nw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.12.5: - resolution: {integrity: sha512-gQYbOhZg5Ww0bQ/bC0w/4W6yQRwBumUUnkB+QPo15VznwxZe2a7bo6JM+9Xy9dKLa/kn+p7zTqme4OEp6M3/Yg==} + /turbo-windows-64@1.13.0: + resolution: {integrity: sha512-hJgSZJZwlWHNwLEthaqJqJWGm4NqF5X/I7vE0sPE4i/jeDl8f0n1hcOkgJkJiNXVxhj+qy/9+4dzbPLKT9imaQ==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.12.5: - resolution: {integrity: sha512-auvhZ9FrhnvQ4mgBlY9O68MT4dIfprYGvd2uPICba/mHUZZvVy5SGgbHJ0KbMwaJfnnFoPgLJO6M+3N2gDprKw==} + /turbo-windows-arm64@1.13.0: + resolution: {integrity: sha512-L/ErxYoXeq8tmjU/AIGicC9VyBN1zdYw8JlM4yPmMI0pJdY8E4GaYK1IiIazqq7M72lmQhU/WW7fV9FqEktwrw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.12.5: - resolution: {integrity: sha512-FATU5EnhrYG8RvQJYFJnDd18DpccDjyvd53hggw9T9JEg9BhWtIEoeaKtBjYbpXwOVrJQMDdXcIB4f2nD3QPPg==} + /turbo@1.13.0: + resolution: {integrity: sha512-r02GtNmkOPcQvUzVE6lg474QVLyU02r3yh3lUGqrFHf5h5ZEjgDGWILsAUqplVqjri1Y/oOkTssks4CObTAaiw==} hasBin: true optionalDependencies: - turbo-darwin-64: 1.12.5 - turbo-darwin-arm64: 1.12.5 - turbo-linux-64: 1.12.5 - turbo-linux-arm64: 1.12.5 - turbo-windows-64: 1.12.5 - turbo-windows-arm64: 1.12.5 + turbo-darwin-64: 1.13.0 + turbo-darwin-arm64: 1.13.0 + turbo-linux-64: 1.13.0 + turbo-linux-arm64: 1.13.0 + turbo-windows-64: 1.13.0 + turbo-windows-arm64: 1.13.0 dev: true /type-check@0.3.2: From 6c9f365a2ecfa3ebbb1551ac3da978b5467c2021 Mon Sep 17 00:00:00 2001 From: Simon O <60504110+simonorzel26@users.noreply.github.com> Date: Fri, 12 Apr 2024 23:56:51 +0200 Subject: [PATCH 6/8] feat: adds divider and tooltips --- apps/web/src/components/Editor.tsx | 277 +++++++++++++++++++---------- 1 file changed, 184 insertions(+), 93 deletions(-) diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx index de100b97..a6cf8cfe 100644 --- a/apps/web/src/components/Editor.tsx +++ b/apps/web/src/components/Editor.tsx @@ -1,13 +1,14 @@ import TextAlign from "@tiptap/extension-text-align"; import { - Editor as TipTapEditor, EditorContent, + type Editor as TipTapEditor, useEditor, } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import clsx from "clsx"; -import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Heading1, Heading2, Heading3, Italic, Strikethrough } from "lucide-react"; +import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Heading1, Heading2, Heading3, Italic, Pilcrow, Strikethrough } from "lucide-react"; import { twMerge } from "tailwind-merge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./Tooltip"; type Props = { className?: string; @@ -38,97 +39,187 @@ const MenuBar = ({ editor }: { editor: TipTapEditor | null }) => { } return ( -
- - - - - - - - - - - +
+ + + Heading 1 + + + + + + + + + Heading 2 + + + + + + + + + Heading 3 + + + + + + + + + Text + + + + + + + + + + Bold + + + + + + + + + Italic + + + + + + + + + Strikethrough + + + + + + + + + + Align Left + + + + + + + + + Align Center + + + + + + + + + Align Right + + + + + + + + + Align Justify + + + + + +
); }; From 7267fedb10a19ac91d92fb73c599df7385c8cbe5 Mon Sep 17 00:00:00 2001 From: Simon O <60504110+simonorzel26@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:44:47 +0200 Subject: [PATCH 7/8] feat: removes eslint, adds biome --- apps/cdn/src/index.ts | 66 +- apps/cdn/src/lib/cache.ts | 56 +- apps/cdn/src/lib/config.ts | 38 +- apps/cdn/tsconfig.json | 12 +- apps/docs/next.config.js | 18 +- apps/docs/package.json | 1 - apps/docs/pages/reference/_meta.json | 14 +- apps/docs/theme.config.jsx | 36 +- apps/docs/tsconfig.json | 2 +- apps/web/abby.config.ts | 16 +- apps/web/emails/ContactFormularEmail.tsx | 136 ++- apps/web/emails/index.tsx | 26 +- apps/web/emails/invite.tsx | 147 ++- apps/web/next-sitemap.config.js | 22 +- apps/web/next.config.mjs | 34 +- apps/web/package.json | 1 - apps/web/postcss.config.cjs | 2 +- apps/web/prettier.config.cjs | 4 +- apps/web/prisma/generateCoupons.ts | 26 +- apps/web/prisma/seedEvents.ts | 66 +- apps/web/src/api/index.ts | 34 +- apps/web/src/api/routes/health.test.ts | 26 +- apps/web/src/api/routes/health.ts | 19 +- .../api/routes/legacy_project_data.test.ts | 98 +- .../web/src/api/routes/legacy_project_data.ts | 103 +- apps/web/src/api/routes/v1_config.test.ts | 190 ++-- apps/web/src/api/routes/v1_config.ts | 83 +- apps/web/src/api/routes/v1_event.test.ts | 94 +- apps/web/src/api/routes/v1_event.ts | 125 ++- .../src/api/routes/v1_project_data.test.ts | 146 +-- apps/web/src/api/routes/v1_project_data.ts | 138 ++- apps/web/src/components/AddABTestModal.tsx | 99 +- .../src/components/AddFeatureFlagModal.tsx | 230 ++--- apps/web/src/components/AsyncCodeExample.tsx | 18 +- apps/web/src/components/Avatar.tsx | 36 +- apps/web/src/components/BlogLayout.tsx | 53 +- apps/web/src/components/Button.tsx | 22 +- apps/web/src/components/CodeSnippet.tsx | 94 +- .../src/components/CodeSnippetModalButton.tsx | 66 +- apps/web/src/components/CreateAPIKeyModal.tsx | 65 +- .../src/components/CreateEnvironmentModal.tsx | 81 +- .../web/src/components/CreateProjectModal.tsx | 92 +- apps/web/src/components/DashboardButton.tsx | 8 +- apps/web/src/components/DashboardHeader.tsx | 18 +- apps/web/src/components/DashboardSection.tsx | 30 +- .../web/src/components/DeleteProjectModal.tsx | 62 +- apps/web/src/components/DevtoolsArrow.tsx | 110 +- apps/web/src/components/Divider.tsx | 11 +- apps/web/src/components/Dropdown.tsx | 49 +- apps/web/src/components/DropdownMenu.tsx | 117 +-- apps/web/src/components/Editor.tsx | 120 ++- apps/web/src/components/Feature.tsx | 28 +- apps/web/src/components/FeatureFlag.tsx | 194 ++-- apps/web/src/components/FlagIcon.tsx | 32 +- apps/web/src/components/FlagPage.tsx | 292 +++--- apps/web/src/components/Footer.tsx | 68 +- apps/web/src/components/Input.tsx | 39 +- apps/web/src/components/Integrations.tsx | 123 ++- apps/web/src/components/JSONEditor.tsx | 18 +- apps/web/src/components/Layout.tsx | 90 +- apps/web/src/components/LoadingSpinner.tsx | 45 +- apps/web/src/components/Logo.tsx | 10 +- apps/web/src/components/MarketingLayout.tsx | 76 +- apps/web/src/components/Modal.tsx | 66 +- apps/web/src/components/Navbar/index.tsx | 241 ++--- apps/web/src/components/Pricing.tsx | 189 ++-- apps/web/src/components/Progress.tsx | 23 +- apps/web/src/components/ProjectSwitcher.tsx | 147 ++- apps/web/src/components/RadioGroup.tsx | 40 +- apps/web/src/components/RadioSelect.tsx | 41 +- apps/web/src/components/RemoveUserModal.tsx | 48 +- apps/web/src/components/Select.tsx | 85 +- apps/web/src/components/SignupButton.tsx | 24 +- .../src/components/Test/CreateTestSection.tsx | 169 ++- apps/web/src/components/Test/Metrics.tsx | 67 +- apps/web/src/components/Test/Section.tsx | 191 ++-- apps/web/src/components/Test/Serves.tsx | 81 +- apps/web/src/components/Test/Weights.tsx | 81 +- apps/web/src/components/TitleEdit.tsx | 52 +- apps/web/src/components/Toggle.tsx | 24 +- apps/web/src/components/Tooltip.tsx | 24 +- apps/web/src/components/UsedBy.tsx | 54 +- apps/web/src/components/UserAuthForm.tsx | 92 +- apps/web/src/components/UserInfo.tsx | 44 +- apps/web/src/components/app/AppNav.tsx | 45 +- apps/web/src/components/app/UserNav.tsx | 46 +- apps/web/src/components/ui/avatar.tsx | 15 +- apps/web/src/components/ui/button.tsx | 57 +- apps/web/src/components/ui/card.tsx | 119 +-- apps/web/src/components/ui/command.tsx | 50 +- apps/web/src/components/ui/dialog.tsx | 52 +- apps/web/src/components/ui/input.tsx | 17 +- apps/web/src/components/ui/label.tsx | 19 +- .../web/src/components/ui/navigation-menu.tsx | 74 +- apps/web/src/components/ui/popover.tsx | 10 +- apps/web/src/components/ui/radio-group.tsx | 24 +- apps/web/src/components/ui/select.tsx | 64 +- apps/web/src/components/ui/switch.tsx | 18 +- apps/web/src/env/client.mjs | 28 +- apps/web/src/env/schema.mjs | 29 +- apps/web/src/env/server.mjs | 21 +- apps/web/src/lib/abby.tsx | 10 +- apps/web/src/lib/events.ts | 70 +- apps/web/src/lib/flags.ts | 86 +- apps/web/src/lib/graphs.ts | 38 +- apps/web/src/lib/helper.ts | 48 +- apps/web/src/lib/hooks/useProjectId.ts | 6 +- apps/web/src/lib/hooks/useQueryParam.ts | 14 +- apps/web/src/lib/logsnag.ts | 32 +- apps/web/src/lib/stripe.ts | 51 +- apps/web/src/lib/tracking.ts | 6 +- apps/web/src/lib/utils.ts | 6 +- apps/web/src/middleware.ts | 56 +- apps/web/src/pages/_app.tsx | 80 +- apps/web/src/pages/_document.tsx | 16 +- apps/web/src/pages/api/[[...route]].ts | 8 +- apps/web/src/pages/api/auth/[...nextauth].ts | 70 +- apps/web/src/pages/api/checkout/index.ts | 16 +- apps/web/src/pages/api/invalidate-limits.ts | 40 +- apps/web/src/pages/api/stripe.ts | 95 +- apps/web/src/pages/api/trpc/[trpc].ts | 14 +- apps/web/src/pages/checkout/index.tsx | 22 +- apps/web/src/pages/contact.tsx | 151 ++- apps/web/src/pages/devtools.tsx | 216 ++-- apps/web/src/pages/index.tsx | 673 ++++++------ .../integrations/[integration]/index.tsx | 109 +- apps/web/src/pages/integrations/index.tsx | 48 +- apps/web/src/pages/invites/[inviteId].tsx | 75 +- apps/web/src/pages/invites/index.tsx | 14 +- apps/web/src/pages/login.tsx | 81 +- apps/web/src/pages/profile/generate-token.tsx | 58 +- apps/web/src/pages/profile/index.tsx | 299 +++--- .../projects/[projectId]/environments.tsx | 227 ++-- .../src/pages/projects/[projectId]/flags.tsx | 42 +- .../src/pages/projects/[projectId]/index.tsx | 68 +- .../src/pages/projects/[projectId]/redeem.tsx | 76 +- .../projects/[projectId]/remote-config.tsx | 32 +- .../pages/projects/[projectId]/settings.tsx | 333 +++--- .../projects/[projectId]/tests/[testId].tsx | 155 ++- apps/web/src/pages/redeem.tsx | 36 +- apps/web/src/pages/signup.tsx | 78 +- .../web/src/pages/tips-and-insights/index.tsx | 84 +- apps/web/src/pages/welcome.tsx | 269 +++-- apps/web/src/seo/SeoDescriptions.ts | 249 +++-- .../server/common/get-server-auth-session.ts | 14 +- .../web/src/server/common/getRequestOrigin.ts | 6 +- apps/web/src/server/common/memory-cache.ts | 6 +- apps/web/src/server/common/plans.ts | 52 +- apps/web/src/server/common/stripe.ts | 14 +- apps/web/src/server/db/client.ts | 30 +- apps/web/src/server/db/redis.ts | 12 +- apps/web/src/server/queue/AfterDataRequest.ts | 44 +- apps/web/src/server/queue/Manager.ts | 28 +- apps/web/src/server/queue/types.ts | 18 +- apps/web/src/server/services/ConfigService.ts | 122 +-- apps/web/src/server/services/EventService.ts | 49 +- apps/web/src/server/services/FlagService.ts | 48 +- apps/web/src/server/services/InviteService.ts | 20 +- .../src/server/services/PlausibleService.ts | 24 +- .../web/src/server/services/ProjectService.ts | 14 +- apps/web/src/server/services/RequestCache.ts | 14 +- .../web/src/server/services/RequestService.ts | 8 +- apps/web/src/server/services/TestService.ts | 26 +- apps/web/src/server/trpc/context.ts | 32 +- apps/web/src/server/trpc/helpers.ts | 12 +- apps/web/src/server/trpc/router/_app.ts | 32 +- apps/web/src/server/trpc/router/apikey.ts | 22 +- apps/web/src/server/trpc/router/auth.ts | 8 +- apps/web/src/server/trpc/router/coupons.ts | 20 +- .../src/server/trpc/router/environments.ts | 58 +- apps/web/src/server/trpc/router/events.ts | 32 +- apps/web/src/server/trpc/router/example.ts | 44 +- apps/web/src/server/trpc/router/flags.ts | 66 +- apps/web/src/server/trpc/router/invite.ts | 52 +- apps/web/src/server/trpc/router/misc.ts | 37 +- .../src/server/trpc/router/project-user.ts | 10 +- apps/web/src/server/trpc/router/project.ts | 101 +- apps/web/src/server/trpc/router/tests.ts | 51 +- apps/web/src/server/trpc/router/user.ts | 42 +- apps/web/src/server/trpc/trpc.ts | 22 +- apps/web/src/types/flags.ts | 4 +- apps/web/src/types/next-auth.d.ts | 24 +- apps/web/src/types/plausible-events.ts | 56 +- apps/web/src/utils/apiKey.ts | 16 +- apps/web/src/utils/checkSession.ts | 16 +- apps/web/src/utils/snippets.ts | 121 ++- apps/web/src/utils/trpc.ts | 28 +- apps/web/src/utils/updateSession.ts | 37 +- apps/web/src/utils/validateFlags.ts | 16 +- apps/web/tailwind.config.cjs | 131 ++- apps/web/vitest.config.ts | 10 +- biome.json | 48 + package.json | 7 +- packages/eslint-config-custom/package.json | 3 +- pnpm-lock.yaml | 977 +++--------------- 195 files changed, 6029 insertions(+), 7458 deletions(-) create mode 100644 biome.json diff --git a/apps/cdn/src/index.ts b/apps/cdn/src/index.ts index c2eebb28..cc59be7a 100644 --- a/apps/cdn/src/index.ts +++ b/apps/cdn/src/index.ts @@ -1,66 +1,66 @@ -import { Hono } from "hono"; -import { ZoneCache } from "./lib/cache"; -import { ABBY_WINDOW_KEY, AbbyDataResponse } from "@tryabby/core"; +import { Hono } from 'hono' +import { ZoneCache } from './lib/cache' +import { ABBY_WINDOW_KEY, AbbyDataResponse } from '@tryabby/core' -import { cors } from "hono/cors"; -import { timing } from "hono/timing"; -import { logger } from "hono/logger"; -import { ConfigService } from "./lib/config"; +import { cors } from 'hono/cors' +import { timing } from 'hono/timing' +import { logger } from 'hono/logger' +import { ConfigService } from './lib/config' const cache = new ZoneCache<{ - config: AbbyDataResponse; + config: AbbyDataResponse }>({ - cloudflareApiKey: "", - domain: "cache.tryabby.com", + cloudflareApiKey: '', + domain: 'cache.tryabby.com', fresh: 60 * 1000, stale: 60 * 1000, - zoneId: "", -}); + zoneId: '', +}) -const configCache = new ConfigService(cache); +const configCache = new ConfigService(cache) const app = new Hono() .use( - "*", + '*', cors({ - origin: "*", + origin: '*', maxAge: 60 * 60 * 24 * 30, }) ) - .use("*", timing()) - .use("*", logger()) - .get("/:projectId/:environment", async (c) => { - const environment = c.req.param("environment"); - const projectId = c.req.param("projectId"); + .use('*', timing()) + .use('*', logger()) + .get('/:projectId/:environment', async (c) => { + const environment = c.req.param('environment') + const projectId = c.req.param('projectId') const [data, , reason] = await configCache.retrieveConfig({ c, environment, projectId, - }); + }) - c.header("x-abby-cache", reason); - return c.json(data); + c.header('x-abby-cache', reason) + return c.json(data) }) - .get("/:projectId/:environment/script.js", async (c) => { - const environment = c.req.param("environment"); - const projectId = c.req.param("projectId"); + .get('/:projectId/:environment/script.js', async (c) => { + const environment = c.req.param('environment') + const projectId = c.req.param('projectId') const [data, , reason] = await configCache.retrieveConfig({ c, environment, projectId, - }); + }) - c.header("x-abby-cache", reason); + c.header('x-abby-cache', reason) - const script = `window.${ABBY_WINDOW_KEY} = ${JSON.stringify(data)};`; + const script = `window.${ABBY_WINDOW_KEY} = ${JSON.stringify(data)};` return c.text(script, { headers: { - "Content-Type": "application/javascript", + 'Content-Type': 'application/javascript', }, - }); - }); + }) + }) -export default app; +export default app diff --git a/apps/cdn/src/lib/cache.ts b/apps/cdn/src/lib/cache.ts index 705adfa4..760403e8 100644 --- a/apps/cdn/src/lib/cache.ts +++ b/apps/cdn/src/lib/cache.ts @@ -1,65 +1,63 @@ -import type { Context } from "hono"; +import type { Context } from 'hono' export type CacheConfig = { /** * How long an entry should be fresh in milliseconds */ - fresh: number; + fresh: number /** * How long an entry should be stale in milliseconds * * Stale entries are still valid but should be refreshed in the background */ - stale: number; -}; + stale: number +} export type Entry = { - value: TValue; -}; + value: TValue +} export type ZoneCacheConfig = CacheConfig & { - domain: string; - zoneId: string; + domain: string + zoneId: string /** * This token must have at least */ - cloudflareApiKey: string; -}; + cloudflareApiKey: string +} export class ZoneCache> { - private readonly config: ZoneCacheConfig; + private readonly config: ZoneCacheConfig constructor(config: ZoneCacheConfig) { - this.config = config; + this.config = config } private createCacheKey( namespace: TName, key: string, - cacheBuster = "v1" + cacheBuster = 'v1' ): URL { - return new URL( - `https://${this.config.domain}/cache/${cacheBuster}/${String(namespace)}/${key}` - ); + return new URL(`https://${this.config.domain}/cache/${cacheBuster}/${String(namespace)}/${key}`) } public async get( c: Context, namespace: TName, key: string - ): Promise<[TNamespaces[TName] | undefined, "stale" | "hit" | "miss" | "error"]> { + ): Promise<[TNamespaces[TName] | undefined, 'stale' | 'hit' | 'miss' | 'error']> { try { - const res = await caches.default.match(new Request(this.createCacheKey(namespace, key))); + const res = await caches.default.match(new Request(this.createCacheKey(namespace, key))) if (!res) { - return [undefined, "miss"]; + return [undefined, 'miss'] } - const entry = (await res.json()) as Entry; + const entry = (await res.json()) as Entry - return [entry.value, "hit"]; + return [entry.value, 'hit'] } catch (e) { - console.error("zone cache error:", e); - return [undefined, "error"]; + console.error('zone cache error:', e) + return [undefined, 'error'] } } @@ -71,15 +69,15 @@ export class ZoneCache> { ): Promise { const entry: Entry = { value: value, - }; - const req = new Request(this.createCacheKey(namespace, key)); + } + const req = new Request(this.createCacheKey(namespace, key)) const res = new Response(JSON.stringify(entry), { headers: { - "Content-Type": "application/json", - "Cache-Control": `public, s-maxage=60`, + 'Content-Type': 'application/json', + 'Cache-Control': `public, s-maxage=60`, }, - }); + }) - await caches.default.put(req, res); + await caches.default.put(req, res) } } diff --git a/apps/cdn/src/lib/config.ts b/apps/cdn/src/lib/config.ts index 06555095..3c92840b 100644 --- a/apps/cdn/src/lib/config.ts +++ b/apps/cdn/src/lib/config.ts @@ -1,13 +1,13 @@ -import { AbbyDataResponse, HttpService } from "@tryabby/core"; +import { AbbyDataResponse, HttpService } from '@tryabby/core' -import type { ZoneCache } from "./cache"; -import { Context } from "hono"; -import { endTime, startTime } from "hono/timing"; +import type { ZoneCache } from './cache' +import { Context } from 'hono' +import { endTime, startTime } from 'hono/timing' export class ConfigService { constructor( private readonly cache: ZoneCache<{ - config: AbbyDataResponse; + config: AbbyDataResponse }> ) {} @@ -16,35 +16,35 @@ export class ConfigService { projectId, c, }: { - projectId: string; - environment: string; - c: Context; + projectId: string + environment: string + c: Context }) { - const cacheKey = [projectId, environment].join(","); + const cacheKey = [projectId, environment].join(',') - startTime(c, "cacheRead"); - const [cachedData, reason] = await this.cache.get(c, "config", cacheKey); + startTime(c, 'cacheRead') + const [cachedData, reason] = await this.cache.get(c, 'config', cacheKey) - endTime(c, "cacheRead"); + endTime(c, 'cacheRead') if (cachedData) { - return [cachedData, true, reason] as const; + return [cachedData, true, reason] as const } - startTime(c, "remoteRead"); + startTime(c, 'remoteRead') const data = await HttpService.getProjectData({ projectId, environment, - }); + }) if (!data) { - throw new Error("Failed to fetch data"); + throw new Error('Failed to fetch data') } - endTime(c, "remoteRead"); - c.executionCtx.waitUntil(this.cache.set(c, "config", cacheKey, data)); + endTime(c, 'remoteRead') + c.executionCtx.waitUntil(this.cache.set(c, 'config', cacheKey, data)) - return [data, false, reason] as const; + return [data, false, reason] as const } } diff --git a/apps/cdn/tsconfig.json b/apps/cdn/tsconfig.json index 9cd84898..4a2abdbe 100644 --- a/apps/cdn/tsconfig.json +++ b/apps/cdn/tsconfig.json @@ -5,13 +5,9 @@ "moduleResolution": "node", "esModuleInterop": true, "strict": true, - "lib": [ - "esnext" - ], - "types": [ - "@cloudflare/workers-types" - ], + "lib": ["esnext"], + "types": ["@cloudflare/workers-types"], "jsx": "react-jsx", "jsxImportSource": "hono/jsx" - }, -} \ No newline at end of file + } +} diff --git a/apps/docs/next.config.js b/apps/docs/next.config.js index 755c6edb..b3bc0ad7 100644 --- a/apps/docs/next.config.js +++ b/apps/docs/next.config.js @@ -1,10 +1,10 @@ -const withNextra = require("nextra")({ - theme: "nextra-theme-docs", - themeConfig: "./theme.config.jsx", +const withNextra = require('nextra')({ + theme: 'nextra-theme-docs', + themeConfig: './theme.config.jsx', defaultShowCopyCode: true, -}); +}) -const { withPlausibleProxy } = require("next-plausible"); +const { withPlausibleProxy } = require('next-plausible') /** @type {import('next').NextConfig} */ const nextConfig = { @@ -12,9 +12,9 @@ const nextConfig = { swcMinify: true, // use this to add to all pages i18n: { - locales: ["en"], - defaultLocale: "en", + locales: ['en'], + defaultLocale: 'en', }, -}; +} -module.exports = withPlausibleProxy()(withNextra(nextConfig)); +module.exports = withPlausibleProxy()(withNextra(nextConfig)) diff --git a/apps/docs/package.json b/apps/docs/package.json index 99bcf71c..08aa2e8a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -13,7 +13,6 @@ "@types/react": "18.0.26", "@types/react-dom": "^18.0.5", "eslint": "8.29.0", - "eslint-config-next": "13.0.6", "next": "14.0.4", "next-plausible": "^3.11.3", "nextra": "2.13.2", diff --git a/apps/docs/pages/reference/_meta.json b/apps/docs/pages/reference/_meta.json index 8322e2ba..abf92dd4 100644 --- a/apps/docs/pages/reference/_meta.json +++ b/apps/docs/pages/reference/_meta.json @@ -1,8 +1,8 @@ { - "nextjs": "Next.js", - "react": "React", - "svelte": "Svelte", - "angular": "Angular", - "http": "HTTP API", - "cli": "CLI" -} \ No newline at end of file + "nextjs": "Next.js", + "react": "React", + "svelte": "Svelte", + "angular": "Angular", + "http": "HTTP API", + "cli": "CLI" +} diff --git a/apps/docs/theme.config.jsx b/apps/docs/theme.config.jsx index 4f35f11e..80697784 100644 --- a/apps/docs/theme.config.jsx +++ b/apps/docs/theme.config.jsx @@ -1,43 +1,43 @@ /* eslint-disable import/no-anonymous-default-export */ -import { useRouter } from "next/router"; +import { useRouter } from 'next/router' export default { useNextSeoProps() { - const router = useRouter(); - const currentPageUrl = `https://docs.tryabby.com${router.asPath}`; + const router = useRouter() + const currentPageUrl = `https://docs.tryabby.com${router.asPath}` return { - titleTemplate: "%s – Abby Docs", + titleTemplate: '%s – Abby Docs', description: - "Abby is a SaaS tool for developers to streamline A/B testing and feature flagging. Make data-driven decisions and improve user experience with ease.", + 'Abby is a SaaS tool for developers to streamline A/B testing and feature flagging. Make data-driven decisions and improve user experience with ease.', openGraph: { url: currentPageUrl, - title: "Abby Docs", - type: "website", + title: 'Abby Docs', + type: 'website', description: - "Abby is a SaaS tool for developers to streamline A/B testing and feature flagging. Make data-driven decisions and improve user experience with ease.", + 'Abby is a SaaS tool for developers to streamline A/B testing and feature flagging. Make data-driven decisions and improve user experience with ease.', images: [ { - url: "https://www.tryabby.com/og.png", + url: 'https://www.tryabby.com/og.png', width: 1200, height: 630, - alt: "Abby", - type: "image/png", + alt: 'Abby', + type: 'image/png', }, ], - siteName: "Abby Docs", + siteName: 'Abby Docs', }, - }; + } }, search: { - loading: "Loading...", - placeholder: "Search...", + loading: 'Loading...', + placeholder: 'Search...', }, head: ( <> - + ), - logo: Abby, + logo: Abby, // docsRepositoryBase: 'https://github.com/cstrnt/abby/blob/main/apps/docs/pages', project: { // link: "https://github.com/cstrnt/abby", @@ -55,4 +55,4 @@ export default { component: null, }, // ... -}; +} diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index 735e1de8..e6e7302a 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -2,4 +2,4 @@ "extends": "tsconfig/nextjs.json", "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "theme.config.jsx"], "exclude": ["node_modules"] -} \ No newline at end of file +} diff --git a/apps/web/abby.config.ts b/apps/web/abby.config.ts index 2fcd4947..ccbef7a5 100644 --- a/apps/web/abby.config.ts +++ b/apps/web/abby.config.ts @@ -1,5 +1,5 @@ /* eslint-disable turbo/no-undeclared-env-vars */ -import { defineConfig } from "@tryabby/core"; +import { defineConfig } from '@tryabby/core' export default defineConfig( { @@ -7,21 +7,21 @@ export default defineConfig( currentEnvironment: process.env.VERCEL_ENV ?? process.env.NODE_ENV, apiUrl: process.env.NEXT_PUBLIC_ABBY_API_URL, __experimentalCdnUrl: process.env.NEXT_PUBLIC_ABBY_CDN_URL, - debug: process.env.NEXT_PUBLIC_ABBY_DEBUG === "true", + debug: process.env.NEXT_PUBLIC_ABBY_DEBUG === 'true', }, { - environments: ["development", "production"], + environments: ['development', 'production'], tests: { SignupButton: { - variants: ["A", "B"], + variants: ['A', 'B'], }, TipsAndTricks: { - variants: ["Blog"], + variants: ['Blog'], }, }, - flags: ["AdvancedTestStats", "showFooter", "test"], + flags: ['AdvancedTestStats', 'showFooter', 'test'], remoteConfig: { - abc: "JSON", + abc: 'JSON', }, } -); +) diff --git a/apps/web/emails/ContactFormularEmail.tsx b/apps/web/emails/ContactFormularEmail.tsx index 51d2fb6b..b028a1cb 100644 --- a/apps/web/emails/ContactFormularEmail.tsx +++ b/apps/web/emails/ContactFormularEmail.tsx @@ -1,42 +1,34 @@ -import { Project, User } from "@prisma/client"; -import { Button } from "@react-email/button"; -import { Container } from "@react-email/container"; -import { Head } from "@react-email/head"; -import { Hr } from "@react-email/hr"; -import { Html } from "@react-email/html"; -import { Img } from "@react-email/img"; -import { Link } from "@react-email/link"; -import { Preview } from "@react-email/preview"; -import { Section } from "@react-email/section"; -import { Text } from "@react-email/text"; -import { env } from "env/server.mjs"; -import * as React from "react"; -import { ABBY_BASE_URL } from "@tryabby/core"; +import { Project, User } from '@prisma/client' +import { Button } from '@react-email/button' +import { Container } from '@react-email/container' +import { Head } from '@react-email/head' +import { Hr } from '@react-email/hr' +import { Html } from '@react-email/html' +import { Img } from '@react-email/img' +import { Link } from '@react-email/link' +import { Preview } from '@react-email/preview' +import { Section } from '@react-email/section' +import { Text } from '@react-email/text' +import { env } from 'env/server.mjs' +import * as React from 'react' +import { ABBY_BASE_URL } from '@tryabby/core' export type Props = { - surname: string; - name: string; - mailadress: string; - message: string; -}; + surname: string + name: string + mailadress: string + message: string +} -export default function ContactFormularEmail({ - surname, - mailadress, - name, - message, -}: Props) { - const baseUrl = - process.env.NODE_ENV === "development" - ? "http://localhost:3000/" - : ABBY_BASE_URL; +export default function ContactFormularEmail({ surname, mailadress, name, message }: Props) { + const baseUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/' : ABBY_BASE_URL return ( {/* @ts-ignore types are off */} - {name} {surname} tried to contact{" "} + {name} {surname} tried to contact{' '}
@@ -50,75 +42,75 @@ export default function ContactFormularEmail({
- ); + ) } const main = { - backgroundColor: "#ffffff", - margin: "0 auto", -}; + backgroundColor: '#ffffff', + margin: '0 auto', +} const container = { - border: "1px solid #eaeaea", - borderRadius: "5px", - margin: "40px auto", - padding: "20px", - width: "465px", -}; + border: '1px solid #eaeaea', + borderRadius: '5px', + margin: '40px auto', + padding: '20px', + width: '465px', +} const logo = { - margin: "0 auto", -}; + margin: '0 auto', +} const h1 = { - color: "#000", + color: '#000', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", - fontSize: "24px", - fontWeight: "normal", - textAlign: "center" as const, - margin: "30px 0", - padding: "0", -}; + fontSize: '24px', + fontWeight: 'normal', + textAlign: 'center' as const, + margin: '30px 0', + padding: '0', +} const avatar = { - borderRadius: "100%", -}; + borderRadius: '100%', +} const link = { - color: "#067df7", - textDecoration: "none", -}; + color: '#067df7', + textDecoration: 'none', +} const text = { - color: "#000", + color: '#000', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", - fontSize: "14px", - lineHeight: "24px", -}; + fontSize: '14px', + lineHeight: '24px', +} const black = { - color: "black", -}; + color: 'black', +} const center = { - verticalAlign: "middle", -}; + verticalAlign: 'middle', +} const btn = { - backgroundColor: "#000", - borderRadius: "5px", - color: "#fff", + backgroundColor: '#000', + borderRadius: '5px', + color: '#fff', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", - fontSize: "12px", + fontSize: '12px', fontWeight: 500, - lineHeight: "50px", - textDecoration: "none", - textAlign: "center" as const, -}; + lineHeight: '50px', + textDecoration: 'none', + textAlign: 'center' as const, +} const spacing = { - marginBottom: "26px", -}; + marginBottom: '26px', +} diff --git a/apps/web/emails/index.tsx b/apps/web/emails/index.tsx index c0f5120b..71543d08 100644 --- a/apps/web/emails/index.tsx +++ b/apps/web/emails/index.tsx @@ -1,36 +1,34 @@ -import { Project, User } from "@prisma/client"; -import { render } from "@react-email/render"; -import { env } from "env/server.mjs"; -import { createTransport } from "nodemailer"; -import InviteEmail, { Props as InviteEmailProps } from "./invite"; -import ContactFormularEmail, { - Props as ContactMailProps, -} from "./ContactFormularEmail"; +import { Project, User } from '@prisma/client' +import { render } from '@react-email/render' +import { env } from 'env/server.mjs' +import { createTransport } from 'nodemailer' +import InviteEmail, { Props as InviteEmailProps } from './invite' +import ContactFormularEmail, { Props as ContactMailProps } from './ContactFormularEmail' const transporter = createTransport({ pool: true, url: env.EMAIL_SERVER, from: `Abby <${env.ABBY_FROM_EMAIL}>`, -}); +}) export function sendInviteEmail(props: InviteEmailProps) { - const email = render(); + const email = render() return transporter.sendMail({ to: props.invitee.email, from: `Abby <${env.ABBY_FROM_EMAIL}>`, subject: `Join ${props.inviter.name} on Abby`, html: email, - }); + }) } export function sendContactFormularEmail(props: ContactMailProps) { - const email = render(); - const abbyContactAdress = "tim@tryabby.com"; + const email = render() + const abbyContactAdress = 'tim@tryabby.com' return transporter.sendMail({ to: abbyContactAdress, from: `Abby <${env.ABBY_FROM_EMAIL}>`, subject: `New Message from ${props.name} ${props.surname}`, html: email, - }); + }) } diff --git a/apps/web/emails/invite.tsx b/apps/web/emails/invite.tsx index 3cbdb6e0..0d384281 100644 --- a/apps/web/emails/invite.tsx +++ b/apps/web/emails/invite.tsx @@ -1,30 +1,27 @@ -import { Project, User } from "@prisma/client"; -import { Button } from "@react-email/button"; -import { Container } from "@react-email/container"; -import { Head } from "@react-email/head"; -import { Hr } from "@react-email/hr"; -import { Html } from "@react-email/html"; -import { Img } from "@react-email/img"; -import { Link } from "@react-email/link"; -import { Preview } from "@react-email/preview"; -import { Section } from "@react-email/section"; -import { Text } from "@react-email/text"; -import { env } from "env/server.mjs"; -import * as React from "react"; -import { ABBY_BASE_URL } from "@tryabby/core"; +import { Project, User } from '@prisma/client' +import { Button } from '@react-email/button' +import { Container } from '@react-email/container' +import { Head } from '@react-email/head' +import { Hr } from '@react-email/hr' +import { Html } from '@react-email/html' +import { Img } from '@react-email/img' +import { Link } from '@react-email/link' +import { Preview } from '@react-email/preview' +import { Section } from '@react-email/section' +import { Text } from '@react-email/text' +import { env } from 'env/server.mjs' +import * as React from 'react' +import { ABBY_BASE_URL } from '@tryabby/core' export type Props = { - inviteId: string; - invitee: { name?: string; email: string }; - inviter: { name: string; email: string }; - project: Project; -}; + inviteId: string + invitee: { name?: string; email: string } + inviter: { name: string; email: string } + project: Project +} export default function Email({ inviteId, invitee, inviter, project }: Props) { - const baseUrl = - process.env.NODE_ENV === "development" - ? "http://localhost:3000/" - : ABBY_BASE_URL; + const baseUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/' : ABBY_BASE_URL return ( @@ -33,7 +30,7 @@ export default function Email({ inviteId, invitee, inviter, project }: Props) { Join {inviter.name} on Abby
-
+

Abby

@@ -45,27 +42,21 @@ export default function Email({ inviteId, invitee, inviter, project }: Props) { {inviter.email} - ) has invited you to the {project.name} team on{" "} - Abby. + ) has invited you to the {project.name} team on Abby. -
-

- or copy and paste this URL into your browser:{" "} + or copy and paste this URL into your browser:{' '} {baseUrl}invites/{inviteId} @@ -73,75 +64,75 @@ export default function Email({ inviteId, invitee, inviter, project }: Props) {
- ); + ) } const main = { - backgroundColor: "#ffffff", - margin: "0 auto", -}; + backgroundColor: '#ffffff', + margin: '0 auto', +} const container = { - border: "1px solid #eaeaea", - borderRadius: "5px", - margin: "40px auto", - padding: "20px", - width: "465px", -}; + border: '1px solid #eaeaea', + borderRadius: '5px', + margin: '40px auto', + padding: '20px', + width: '465px', +} const logo = { - margin: "0 auto", -}; + margin: '0 auto', +} const h1 = { - color: "#000", + color: '#000', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", - fontSize: "24px", - fontWeight: "normal", - textAlign: "center" as const, - margin: "30px 0", - padding: "0", -}; + fontSize: '24px', + fontWeight: 'normal', + textAlign: 'center' as const, + margin: '30px 0', + padding: '0', +} const avatar = { - borderRadius: "100%", -}; + borderRadius: '100%', +} const link = { - color: "#067df7", - textDecoration: "none", -}; + color: '#067df7', + textDecoration: 'none', +} const text = { - color: "#000", + color: '#000', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", - fontSize: "14px", - lineHeight: "24px", -}; + fontSize: '14px', + lineHeight: '24px', +} const black = { - color: "black", -}; + color: 'black', +} const center = { - verticalAlign: "middle", -}; + verticalAlign: 'middle', +} const btn = { - backgroundColor: "#000", - borderRadius: "5px", - color: "#fff", + backgroundColor: '#000', + borderRadius: '5px', + color: '#fff', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", - fontSize: "12px", + fontSize: '12px', fontWeight: 500, - lineHeight: "50px", - textDecoration: "none", - textAlign: "center" as const, -}; + lineHeight: '50px', + textDecoration: 'none', + textAlign: 'center' as const, +} const spacing = { - marginBottom: "26px", -}; + marginBottom: '26px', +} diff --git a/apps/web/next-sitemap.config.js b/apps/web/next-sitemap.config.js index 19c72c4b..b4469c79 100644 --- a/apps/web/next-sitemap.config.js +++ b/apps/web/next-sitemap.config.js @@ -1,17 +1,17 @@ /** @type {import('next-sitemap').IConfig} */ module.exports = { - siteUrl: process.env.SITE_URL || "https://www.tryabby.com", + siteUrl: process.env.SITE_URL || 'https://www.tryabby.com', generateRobotsTxt: true, // (optional) exclude: [ - "/test", - "/checkout", - "/projects", - "/invites", - "/marketing/*", - "/redeem", - "/profile", - "/profile/*", - "/welcome", + '/test', + '/checkout', + '/projects', + '/invites', + '/marketing/*', + '/redeem', + '/profile', + '/profile/*', + '/welcome', ], // ...other options -}; +} diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 6950b90f..8da34947 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,9 +1,9 @@ // @ts-check -import bundleAnalzyer from "@next/bundle-analyzer"; -import { withPlausibleProxy } from "next-plausible"; -import mdx from "@next/mdx"; -import { remarkCodeHike } from "@code-hike/mdx"; -import theme from "shiki/themes/poimandres.json" assert { type: "json" }; +import bundleAnalzyer from '@next/bundle-analyzer' +import { withPlausibleProxy } from 'next-plausible' +import mdx from '@next/mdx' +import { remarkCodeHike } from '@code-hike/mdx' +import theme from 'shiki/themes/poimandres.json' assert { type: 'json' } const withMDX = mdx({ extension: /\.mdx?$/, @@ -11,40 +11,38 @@ const withMDX = mdx({ // If you use remark-gfm, you'll need to use next.config.mjs // as the package is ESM only // https://github.com/remarkjs/remark-gfm#install - remarkPlugins: [ - [remarkCodeHike, { theme, lineNumbers: true, showCopyButton: true }], - ], + remarkPlugins: [[remarkCodeHike, { theme, lineNumbers: true, showCopyButton: true }]], rehypePlugins: [], // If you use `MDXProvider`, uncomment the following line. // providerImportSource: "@mdx-js/react", }, -}); +}) const withBundleAnalyzer = bundleAnalzyer({ - enabled: process.env.ANALYZE === "true", -}); + enabled: process.env.ANALYZE === 'true', +}) /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. * This is especially useful for Docker builds. */ -!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs")); +!process.env.SKIP_ENV_VALIDATION && (await import('./src/env/server.mjs')) /** @type {import("next").NextConfig} */ const config = { reactStrictMode: true, swcMinify: true, - pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], - transpilePackages: ["lodash-es"], + pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], + transpilePackages: ['lodash-es'], i18n: { - locales: ["en"], - defaultLocale: "en", + locales: ['en'], + defaultLocale: 'en', }, -}; +} export default withPlausibleProxy()( withBundleAnalyzer( // @ts-ignore withMDX(config) ) -); +) diff --git a/apps/web/package.json b/apps/web/package.json index f7440fdb..11d543cb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -134,7 +134,6 @@ "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.14", "eslint": "^8.38.0", - "eslint-config-next": "13.0.2", "jsdom": "^20.0.3", "postcss": "^8.4.21", "prettier": "^2.8.7", diff --git a/apps/web/postcss.config.cjs b/apps/web/postcss.config.cjs index 12a703d9..33ad091d 100644 --- a/apps/web/postcss.config.cjs +++ b/apps/web/postcss.config.cjs @@ -3,4 +3,4 @@ module.exports = { tailwindcss: {}, autoprefixer: {}, }, -}; +} diff --git a/apps/web/prettier.config.cjs b/apps/web/prettier.config.cjs index 58b0aee2..09275543 100644 --- a/apps/web/prettier.config.cjs +++ b/apps/web/prettier.config.cjs @@ -1,4 +1,4 @@ /** @type {import("prettier").Config} */ module.exports = { - plugins: [require.resolve("prettier-plugin-tailwindcss")], -}; + plugins: [require.resolve('prettier-plugin-tailwindcss')], +} diff --git a/apps/web/prisma/generateCoupons.ts b/apps/web/prisma/generateCoupons.ts index 3c487cb5..dd63ce71 100644 --- a/apps/web/prisma/generateCoupons.ts +++ b/apps/web/prisma/generateCoupons.ts @@ -1,29 +1,29 @@ -import { PrismaClient } from "@prisma/client"; -import fs from "node:fs/promises"; -import path from "node:path"; +import { PrismaClient } from '@prisma/client' +import fs from 'node:fs/promises' +import path from 'node:path' -const prisma = new PrismaClient(); +const prisma = new PrismaClient() -const COUPON_CODE_AMOUNT = 200; +const COUPON_CODE_AMOUNT = 200 async function main() { - const fileName = path.join(__dirname, "./coupons.csv"); + const fileName = path.join(__dirname, './coupons.csv') const items = Array.from({ length: COUPON_CODE_AMOUNT }).map(() => ({ stripePriceId: // eslint-disable-next-line turbo/no-undeclared-env-vars - "STARTUP_LIFETIME", - })); + 'STARTUP_LIFETIME', + })) const codes = await prisma.$transaction( items.map((item) => prisma.couponCodes.create({ data: item })) - ); + ) - const csv = codes.map((code) => code.code).join("\n"); + const csv = codes.map((code) => code.code).join('\n') - await fs.writeFile(fileName, csv); + await fs.writeFile(fileName, csv) - console.log(`Wrote ${COUPON_CODE_AMOUNT} codes to ${fileName}`); + console.log(`Wrote ${COUPON_CODE_AMOUNT} codes to ${fileName}`) } -main(); +main() diff --git a/apps/web/prisma/seedEvents.ts b/apps/web/prisma/seedEvents.ts index dd6f1449..0e3b2ce2 100644 --- a/apps/web/prisma/seedEvents.ts +++ b/apps/web/prisma/seedEvents.ts @@ -1,75 +1,73 @@ -import { PrismaClient, Prisma } from "@prisma/client"; -import { AbbyEventType } from "@tryabby/core"; +import { PrismaClient, Prisma } from '@prisma/client' +import { AbbyEventType } from '@tryabby/core' -const prisma = new PrismaClient(); +const prisma = new PrismaClient() async function main() { const user = await prisma.user.findFirst({ where: {}, include: { projects: true }, - }); + }) if (!user) { - throw new Error( - "User for email not found. Please create an account first." - ); + throw new Error('User for email not found. Please create an account first.') } if (!user.projects[0]) { - throw new Error("User has no projects. Please create a project first."); + throw new Error('User has no projects. Please create a project first.') } const footerTest = await prisma.test.upsert({ where: { projectId_name: { - name: "Footer", + name: 'Footer', projectId: user.projects[0].projectId, }, }, - create: { name: "Footer", projectId: user.projects[0].projectId }, + create: { name: 'Footer', projectId: user.projects[0].projectId }, update: {}, - }); + }) const ctaButtonTest = await prisma.test.upsert({ where: { projectId_name: { - name: "CTA Button", + name: 'CTA Button', projectId: user.projects[0].projectId, }, }, - create: { name: "CTA Button", projectId: user.projects[0].projectId }, + create: { name: 'CTA Button', projectId: user.projects[0].projectId }, update: {}, - }); + }) await prisma.option.createMany({ data: [ { - identifier: "oldFooter", + identifier: 'oldFooter', chance: 0.5, testId: footerTest.id, }, { - identifier: "newFooter", + identifier: 'newFooter', chance: 0.5, testId: footerTest.id, }, { - identifier: "dark", + identifier: 'dark', chance: 0.33, testId: ctaButtonTest.id, }, { - identifier: "light", + identifier: 'light', chance: 0.33, testId: ctaButtonTest.id, }, { - identifier: "cyperpunk", + identifier: 'cyperpunk', chance: 0.33, testId: ctaButtonTest.id, }, ], - }); + }) await prisma.event.createMany({ data: [ @@ -78,51 +76,51 @@ async function main() { }).map( () => ({ - selectedVariant: "oldFooter", + selectedVariant: 'oldFooter', testId: footerTest.id, type: AbbyEventType.PING, - } as Prisma.EventCreateManyInput) + }) as Prisma.EventCreateManyInput ), ...Array.from({ length: Math.floor(Math.random() * 200), }).map( () => ({ - selectedVariant: "newFooter", + selectedVariant: 'newFooter', testId: footerTest.id, type: AbbyEventType.PING, - } as Prisma.EventCreateManyInput) + }) as Prisma.EventCreateManyInput ), ...Array.from({ length: Math.floor(Math.random() * 200), }).map( () => ({ - selectedVariant: "oldFooter", + selectedVariant: 'oldFooter', testId: footerTest.id, type: AbbyEventType.ACT, - } as Prisma.EventCreateManyInput) + }) as Prisma.EventCreateManyInput ), ...Array.from({ length: Math.floor(Math.random() * 200), }).map( () => ({ - selectedVariant: "newFooter", + selectedVariant: 'newFooter', testId: footerTest.id, type: AbbyEventType.ACT, - } as Prisma.EventCreateManyInput) + }) as Prisma.EventCreateManyInput ), ], - }); + }) } main() .then(async () => { - await prisma.$disconnect(); + await prisma.$disconnect() }) .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/apps/web/src/api/index.ts b/apps/web/src/api/index.ts index ba18b331..0521c2b2 100644 --- a/apps/web/src/api/index.ts +++ b/apps/web/src/api/index.ts @@ -1,22 +1,22 @@ -import { makeConfigRoute } from "api/routes/v1_config"; -import { makeProjectDataRoute } from "api/routes/v1_project_data"; -import { Hono } from "hono"; -import { cors } from "hono/cors"; -import { logger } from "hono/logger"; -import { makeHealthRoute } from "./routes/health"; -import { makeEventRoute } from "./routes/v1_event"; -import { makeLegacyProjectDataRoute } from "./routes/legacy_project_data"; +import { makeConfigRoute } from 'api/routes/v1_config' +import { makeProjectDataRoute } from 'api/routes/v1_project_data' +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { logger } from 'hono/logger' +import { makeHealthRoute } from './routes/health' +import { makeEventRoute } from './routes/v1_event' +import { makeLegacyProjectDataRoute } from './routes/legacy_project_data' export const app = new Hono() - .basePath("/api") + .basePath('/api') // base middleware - .use("*", logger()) - .use("*", cors({ origin: "*", maxAge: 86400 })) - .route("/health", makeHealthRoute()) + .use('*', logger()) + .use('*', cors({ origin: '*', maxAge: 86400 })) + .route('/health', makeHealthRoute()) // legacy routes - .route("/data", makeEventRoute()) - .route("/dashboard", makeLegacyProjectDataRoute()) + .route('/data', makeEventRoute()) + .route('/dashboard', makeLegacyProjectDataRoute()) // v1 routes - .route("/v1/config", makeConfigRoute()) - .route("/v1/data", makeProjectDataRoute()) - .route("/v1/track", makeEventRoute()); + .route('/v1/config', makeConfigRoute()) + .route('/v1/data', makeProjectDataRoute()) + .route('/v1/track', makeEventRoute()) diff --git a/apps/web/src/api/routes/health.test.ts b/apps/web/src/api/routes/health.test.ts index bc414e0b..941b532b 100644 --- a/apps/web/src/api/routes/health.test.ts +++ b/apps/web/src/api/routes/health.test.ts @@ -1,25 +1,25 @@ -import { testClient } from "hono/testing"; -import { makeHealthRoute } from "./health"; +import { testClient } from 'hono/testing' +import { makeHealthRoute } from './health' -vi.mock("server/db/client", () => ({ +vi.mock('server/db/client', () => ({ prisma: { verificationToken: { count: vi.fn(async () => 1), }, }, -})); +})) -vi.mock("server/db/redis", () => ({ +vi.mock('server/db/redis', () => ({ redis: { - get: vi.fn(async () => "test"), + get: vi.fn(async () => 'test'), }, -})); +})) -it("should work", async () => { - const app = makeHealthRoute(); +it('should work', async () => { + const app = makeHealthRoute() - const res = await testClient(app).index.$get(); + const res = await testClient(app).index.$get() - expect(res.status).toEqual(200); - expect(await res.json()).toEqual({ status: "ok" }); -}); + expect(res.status).toEqual(200) + expect(await res.json()).toEqual({ status: 'ok' }) +}) diff --git a/apps/web/src/api/routes/health.ts b/apps/web/src/api/routes/health.ts index 184c3848..15e6c5af 100644 --- a/apps/web/src/api/routes/health.ts +++ b/apps/web/src/api/routes/health.ts @@ -1,14 +1,11 @@ -import { Hono } from "hono"; -import { prisma } from "server/db/client"; -import { redis } from "server/db/redis"; +import { Hono } from 'hono' +import { prisma } from 'server/db/client' +import { redis } from 'server/db/redis' export function makeHealthRoute() { - const app = new Hono().get("/", async (c) => { - await Promise.allSettled([ - await prisma.verificationToken.count(), - await redis.get("test"), - ]); - return c.json({ status: "ok" }); - }); - return app; + const app = new Hono().get('/', async (c) => { + await Promise.allSettled([await prisma.verificationToken.count(), await redis.get('test')]) + return c.json({ status: 'ok' }) + }) + return app } diff --git a/apps/web/src/api/routes/legacy_project_data.test.ts b/apps/web/src/api/routes/legacy_project_data.test.ts index 31e11bf7..b7fe56a8 100644 --- a/apps/web/src/api/routes/legacy_project_data.test.ts +++ b/apps/web/src/api/routes/legacy_project_data.test.ts @@ -1,42 +1,42 @@ -import { testClient } from "hono/testing"; -import { makeLegacyProjectDataRoute } from "./legacy_project_data"; -import { jobManager } from "server/queue/Manager"; -import { FeatureFlag, FeatureFlagValue, Option, Test } from "@prisma/client"; -import { Decimal } from "@prisma/client/runtime/library"; +import { testClient } from 'hono/testing' +import { makeLegacyProjectDataRoute } from './legacy_project_data' +import { jobManager } from 'server/queue/Manager' +import { FeatureFlag, FeatureFlagValue, Option, Test } from '@prisma/client' +import { Decimal } from '@prisma/client/runtime/library' -vi.mock("../../env/server.mjs", () => ({ +vi.mock('../../env/server.mjs', () => ({ env: {}, -})); +})) -vi.mock("server/queue/Manager", () => ({ +vi.mock('server/queue/Manager', () => ({ jobManager: { emit: vi.fn().mockResolvedValue(null), }, -})); +})) -vi.mock("server/db/client", () => ({ +vi.mock('server/db/client', () => ({ prisma: { featureFlagValue: { findMany: vi.fn().mockResolvedValue([ { - environmentId: "", + environmentId: '', flag: { - name: "First Flag", - type: "BOOLEAN", + name: 'First Flag', + type: 'BOOLEAN', }, - flagId: "", - id: "", - value: "true", + flagId: '', + id: '', + value: 'true', }, - ] satisfies Array }>), + ] satisfies Array }>), }, test: { findMany: vi.fn().mockResolvedValue([ { - id: "", - name: "First Test", + id: '', + name: 'First Test', createdAt: new Date(), - projectId: "", + projectId: '', updatedAt: new Date(), options: [ { @@ -55,57 +55,57 @@ vi.mock("server/db/client", () => ({ }, ] satisfies Array< Test & { - options: Array>; + options: Array> } >), }, }, -})); +})) -vi.mock("server/db/redis", () => ({ +vi.mock('server/db/redis', () => ({ redis: { get: vi.fn(async () => {}), incr: vi.fn(async () => {}), }, -})); +})) afterEach(() => { - vi.clearAllMocks(); -}); + vi.clearAllMocks() +}) -describe("Get Config", () => { - it("should return the correct config", async () => { - const app = makeLegacyProjectDataRoute(); +describe('Get Config', () => { + it('should return the correct config', async () => { + const app = makeLegacyProjectDataRoute() - const res = await testClient(app)[":projectId"].data.$get({ + const res = await testClient(app)[':projectId'].data.$get({ param: { - projectId: "test", + projectId: 'test', }, query: { - environment: "test", + environment: 'test', }, - }); - expect(res.status).toBe(200); - const data = await res.json(); + }) + expect(res.status).toBe(200) + const data = await res.json() // typeguard to make test fail if data is not AbbyDataResponse - if ("error" in data) { - throw new Error("Expected data to not have an error key"); + if ('error' in data) { + throw new Error('Expected data to not have an error key') } - expect((data as any).error).toBeUndefined(); + expect((data as any).error).toBeUndefined() - expect(data.tests).toHaveLength(1); - expect(data.tests?.[0]?.name).toBe("First Test"); - expect(data.tests?.[0]?.weights).toEqual([0.25, 0.25, 0.25, 0.25]); + expect(data.tests).toHaveLength(1) + expect(data.tests?.[0]?.name).toBe('First Test') + expect(data.tests?.[0]?.weights).toEqual([0.25, 0.25, 0.25, 0.25]) - expect(data.flags).toHaveLength(1); - expect(data.flags?.[0]?.name).toBe("First Flag"); - expect(data.flags?.[0]?.isEnabled).toBe(true); + expect(data.flags).toHaveLength(1) + expect(data.flags?.[0]?.name).toBe('First Flag') + expect(data.flags?.[0]?.isEnabled).toBe(true) - expect(vi.mocked(jobManager.emit)).toHaveBeenCalledTimes(1); + expect(vi.mocked(jobManager.emit)).toHaveBeenCalledTimes(1) expect(vi.mocked(jobManager.emit)).toHaveBeenCalledWith( - "after-data-request", + 'after-data-request', expect.objectContaining({}) - ); - }); -}); + ) + }) +}) diff --git a/apps/web/src/api/routes/legacy_project_data.ts b/apps/web/src/api/routes/legacy_project_data.ts index a66b3f04..78fe8128 100644 --- a/apps/web/src/api/routes/legacy_project_data.ts +++ b/apps/web/src/api/routes/legacy_project_data.ts @@ -1,44 +1,44 @@ -import { Context, Hono } from "hono"; -import { endTime, startTime, timing } from "hono/timing"; +import { Context, Hono } from 'hono' +import { endTime, startTime, timing } from 'hono/timing' -import { zValidator } from "@hono/zod-validator"; -import { cors } from "hono/cors"; -import { prisma } from "server/db/client"; +import { zValidator } from '@hono/zod-validator' +import { cors } from 'hono/cors' +import { prisma } from 'server/db/client' -import { LegacyAbbyDataResponse } from "@tryabby/core"; -import { transformFlagValue } from "lib/flags"; -import { trackPlanOverage } from "lib/logsnag"; -import createCache from "server/common/memory-cache"; -import { EventService } from "server/services/EventService"; -import { RequestCache } from "server/services/RequestCache"; -import { RequestService } from "server/services/RequestService"; -import { z } from "zod"; -import { jobManager } from "server/queue/Manager"; +import { LegacyAbbyDataResponse } from '@tryabby/core' +import { transformFlagValue } from 'lib/flags' +import { trackPlanOverage } from 'lib/logsnag' +import createCache from 'server/common/memory-cache' +import { EventService } from 'server/services/EventService' +import { RequestCache } from 'server/services/RequestCache' +import { RequestService } from 'server/services/RequestService' +import { z } from 'zod' +import { jobManager } from 'server/queue/Manager' const configCache = createCache({ - name: "legacyConfigCache", + name: 'legacyConfigCache', expireAfterMilliseconds: 1000 * 10, -}); +}) async function getAbbyResponseWithCache({ environment, projectId, c, }: { - environment: string; - projectId: string; - c: Context; + environment: string + projectId: string + c: Context }) { - startTime(c, "readCache"); - const cachedConfig = configCache.get(projectId + environment); - endTime(c, "readCache"); + startTime(c, 'readCache') + const cachedConfig = configCache.get(projectId + environment) + endTime(c, 'readCache') - c.header("X-Abby-Cache", cachedConfig !== undefined ? "HIT" : "MISS"); + c.header('X-Abby-Cache', cachedConfig !== undefined ? 'HIT' : 'MISS') if (cachedConfig) { - return cachedConfig; + return cachedConfig } - startTime(c, "db"); + startTime(c, 'db') const [tests, flags] = await Promise.all([ prisma.test.findMany({ where: { @@ -53,74 +53,73 @@ async function getAbbyResponseWithCache({ projectId, }, flag: { - type: "BOOLEAN", + type: 'BOOLEAN', }, }, include: { flag: { select: { name: true, type: true } } }, }), - ]); + ]) - endTime(c, "db"); + endTime(c, 'db') const response = { tests: tests.map((test) => ({ name: test.name, weights: test.options.map((o) => o.chance.toNumber()), })), flags: flags.map((flagValue) => { - const value = transformFlagValue(flagValue.value, flagValue.flag.type); + const value = transformFlagValue(flagValue.value, flagValue.flag.type) return { name: flagValue.flag.name, - isEnabled: - flagValue.flag.type === "BOOLEAN" ? value === true : value !== null, - }; + isEnabled: flagValue.flag.type === 'BOOLEAN' ? value === true : value !== null, + } }), - } satisfies LegacyAbbyDataResponse; + } satisfies LegacyAbbyDataResponse - configCache.set(projectId + environment, response); - return response; + configCache.set(projectId + environment, response) + return response } export function makeLegacyProjectDataRoute() { const app = new Hono().get( - "/:projectId/data", + '/:projectId/data', cors({ - origin: "*", + origin: '*', maxAge: 86400, }), zValidator( - "query", + 'query', z.object({ environment: z.string(), }) ), timing(), async (c) => { - const projectId = c.req.param("projectId"); - const { environment } = c.req.valid("query"); + const projectId = c.req.param('projectId') + const { environment } = c.req.valid('query') - const now = performance.now(); + const now = performance.now() try { - startTime(c, "getAbbyResponseWithCache"); + startTime(c, 'getAbbyResponseWithCache') const response = await getAbbyResponseWithCache({ projectId, environment, c, - }); - endTime(c, "getAbbyResponseWithCache"); + }) + endTime(c, 'getAbbyResponseWithCache') - jobManager.emit("after-data-request", { + jobManager.emit('after-data-request', { projectId, - apiVersion: "V0", + apiVersion: 'V0', functionDuration: performance.now() - now, - }); + }) - return c.json(response); + return c.json(response) } catch (e) { - console.error(e); - return c.json({ error: "Internal server error" }, { status: 500 }); + console.error(e) + return c.json({ error: 'Internal server error' }, { status: 500 }) } } - ); - return app; + ) + return app } diff --git a/apps/web/src/api/routes/v1_config.test.ts b/apps/web/src/api/routes/v1_config.test.ts index d6ae3777..80bf656b 100644 --- a/apps/web/src/api/routes/v1_config.test.ts +++ b/apps/web/src/api/routes/v1_config.test.ts @@ -1,33 +1,33 @@ -import { testClient } from "hono/testing"; -import { makeConfigRoute } from "./v1_config"; -import { handleGET, handlePUT } from "server/services/ConfigService"; -import { prisma } from "server/db/client"; +import { testClient } from 'hono/testing' +import { makeConfigRoute } from './v1_config' +import { handleGET, handlePUT } from 'server/services/ConfigService' +import { prisma } from 'server/db/client' -vi.mock("../../env/server.mjs", () => ({ +vi.mock('../../env/server.mjs', () => ({ env: { - HASHING_SECRET: "test", + HASHING_SECRET: 'test', }, -})); +})) const mockConfig = { - environments: ["test"], + environments: ['test'], flags: [], remoteConfig: {}, tests: {}, -} satisfies Awaited>; +} satisfies Awaited> -vi.mock("server/services/ConfigService", () => ({ +vi.mock('server/services/ConfigService', () => ({ handleGET: vi.fn(() => mockConfig), handlePUT: vi.fn(() => mockConfig), -})); +})) -vi.mock("server/db/redis", () => ({ +vi.mock('server/db/redis', () => ({ redis: { - incr: vi.fn(async () => "test"), + incr: vi.fn(async () => 'test'), }, -})); +})) -vi.mock("server/db/client", () => ({ +vi.mock('server/db/client', () => ({ prisma: { apiKey: { findUnique: vi.fn().mockResolvedValue({ @@ -36,187 +36,187 @@ vi.mock("server/db/client", () => ({ }), }, }, -})); +})) -describe("Retreive Config", () => { - it("should work with correct with an API provided", async () => { - const app = makeConfigRoute(); +describe('Retreive Config', () => { + it('should work with correct with an API provided', async () => { + const app = makeConfigRoute() - const res = await testClient(app)[":projectId"].$get( + const res = await testClient(app)[':projectId'].$get( { param: { - projectId: "test", + projectId: 'test', }, }, { headers: { - Authorization: "Bearer test", + Authorization: 'Bearer test', }, } - ); - const data = await res.json(); + ) + const data = await res.json() - expect(res.status).toEqual(200); - expect(data).toEqual(mockConfig); - }); + expect(res.status).toEqual(200) + expect(data).toEqual(mockConfig) + }) - it("should return an error if the API key is not provided", async () => { - const app = makeConfigRoute(); + it('should return an error if the API key is not provided', async () => { + const app = makeConfigRoute() - const res = await testClient(app)[":projectId"].$get({ + const res = await testClient(app)[':projectId'].$get({ param: { - projectId: "test", + projectId: 'test', }, - }); + }) - expect(res.status).toEqual(401); - }); + expect(res.status).toEqual(401) + }) - it("should return an error if the API key is not found", async () => { - const app = makeConfigRoute(); - vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce(null); + it('should return an error if the API key is not found', async () => { + const app = makeConfigRoute() + vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce(null) - const res = await testClient(app)[":projectId"].$get( + const res = await testClient(app)[':projectId'].$get( { param: { - projectId: "test", + projectId: 'test', }, }, { headers: { - Authorization: "Bearer test", + Authorization: 'Bearer test', }, } - ); + ) - expect(res.status).toEqual(401); - }); + expect(res.status).toEqual(401) + }) - it("should return an error if the API key is outdated", async () => { - const app = makeConfigRoute(); + it('should return an error if the API key is outdated', async () => { + const app = makeConfigRoute() vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce({ validUntil: new Date(Date.now() - 1000), revokedAt: null, - } as any); + } as any) - const res = await testClient(app)[":projectId"].$get( + const res = await testClient(app)[':projectId'].$get( { param: { - projectId: "test", + projectId: 'test', }, }, { headers: { - Authorization: "Bearer test", + Authorization: 'Bearer test', }, } - ); + ) - expect(res.status).toEqual(401); - }); + expect(res.status).toEqual(401) + }) - it("should return an error if the API key is revoked", async () => { - const app = makeConfigRoute(); + it('should return an error if the API key is revoked', async () => { + const app = makeConfigRoute() vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce({ validUntil: new Date(Date.now() - 1000), revokedAt: new Date(), - } as any); + } as any) - const res = await testClient(app)[":projectId"].$get( + const res = await testClient(app)[':projectId'].$get( { param: { - projectId: "test", + projectId: 'test', }, }, { headers: { - Authorization: "Bearer test", + Authorization: 'Bearer test', }, } - ); + ) - expect(res.status).toEqual(401); - }); -}); + expect(res.status).toEqual(401) + }) +}) -describe("Update Config", () => { - it("should work with correct with an API provided", async () => { - const app = makeConfigRoute(); +describe('Update Config', () => { + it('should work with correct with an API provided', async () => { + const app = makeConfigRoute() - const res = await testClient(app)[":projectId"].$put( + const res = await testClient(app)[':projectId'].$put( { param: { - projectId: "test", + projectId: 'test', }, json: { - environments: ["test"], + environments: ['test'], flags: [], remoteConfig: {}, tests: {}, - projectId: "test", - apiUrl: "test", + projectId: 'test', + apiUrl: 'test', }, }, { headers: { - Authorization: "Bearer test", + Authorization: 'Bearer test', }, } - ); + ) - expect(res.status).toEqual(200); - expect(vi.mocked(handlePUT)).toHaveBeenCalledTimes(1); - }); + expect(res.status).toEqual(200) + expect(vi.mocked(handlePUT)).toHaveBeenCalledTimes(1) + }) - it("should not work with invalid api keys", async () => { - const app = makeConfigRoute(); + it('should not work with invalid api keys', async () => { + const app = makeConfigRoute() const makeRequest = () => - testClient(app)[":projectId"].$put( + testClient(app)[':projectId'].$put( { param: { - projectId: "test", + projectId: 'test', }, json: { - environments: ["test"], + environments: ['test'], flags: [], remoteConfig: {}, tests: {}, - projectId: "test", - apiUrl: "test", + projectId: 'test', + apiUrl: 'test', }, }, { headers: { - Authorization: "Bearer test", + Authorization: 'Bearer test', }, } - ); + ) - vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce(null); + vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce(null) - let res = await makeRequest(); + let res = await makeRequest() - expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeGreaterThanOrEqual(400) vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce({ validUntil: new Date(Date.now() - 1000), revokedAt: null, - } as any); + } as any) - res = await makeRequest(); + res = await makeRequest() - expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeGreaterThanOrEqual(400) vi.mocked(prisma.apiKey.findUnique).mockResolvedValueOnce({ validUntil: new Date(), revokedAt: new Date(), - } as any); + } as any) - res = await makeRequest(); + res = await makeRequest() - expect(res.status).toBeGreaterThanOrEqual(400); - }); -}); + expect(res.status).toBeGreaterThanOrEqual(400) + }) +}) diff --git a/apps/web/src/api/routes/v1_config.ts b/apps/web/src/api/routes/v1_config.ts index b8080da3..314b1e81 100644 --- a/apps/web/src/api/routes/v1_config.ts +++ b/apps/web/src/api/routes/v1_config.ts @@ -1,83 +1,78 @@ -import { Hono, MiddlewareHandler } from "hono"; -import { zValidator } from "@hono/zod-validator"; -import { prisma } from "server/db/client"; -import { hashString } from "utils/apiKey"; -import * as ConfigService from "server/services/ConfigService"; -import { ApiKey } from "@prisma/client"; -import { abbyConfigSchema } from "@tryabby/core"; +import { Hono, MiddlewareHandler } from 'hono' +import { zValidator } from '@hono/zod-validator' +import { prisma } from 'server/db/client' +import { hashString } from 'utils/apiKey' +import * as ConfigService from 'server/services/ConfigService' +import { ApiKey } from '@prisma/client' +import { abbyConfigSchema } from '@tryabby/core' const apiKeyMiddleware: MiddlewareHandler<{ Variables: { - apiKey: ApiKey; - }; + apiKey: ApiKey + } }> = async (c, next) => { - const apiKey = c.req.header("authorization")?.split(" ")[1]; + const apiKey = c.req.header('authorization')?.split(' ')[1] if (!apiKey) { - return c.json({ error: "API key not provided" }, { status: 401 }); + return c.json({ error: 'API key not provided' }, { status: 401 }) } - const hashedApiKey = hashString(apiKey); + const hashedApiKey = hashString(apiKey) const apiKeyEntry = await prisma.apiKey.findUnique({ where: { hashedKey: hashedApiKey, }, - }); + }) if (!apiKeyEntry || apiKeyEntry.revokedAt !== null) { return c.json( - { error: "API key revoked" }, + { error: 'API key revoked' }, { status: 401, } - ); + ) } if (apiKeyEntry.validUntil.getTime() < Date.now()) { return c.json( - { error: "API key expired" }, + { error: 'API key expired' }, { status: 401, } - ); + ) } - c.set("apiKey", apiKeyEntry); - await next(); -}; + c.set('apiKey', apiKeyEntry) + await next() +} export function makeConfigRoute() { return new Hono() - .get("/:projectId", apiKeyMiddleware, async (c) => { + .get('/:projectId', apiKeyMiddleware, async (c) => { try { const config = await ConfigService.handleGET({ - projectId: c.req.param("projectId"), - }); - return c.json(config); + projectId: c.req.param('projectId'), + }) + return c.json(config) } catch (error) { - console.error(error); - return c.json({ error: "Internal server error" }, { status: 500 }); + console.error(error) + return c.json({ error: 'Internal server error' }, { status: 500 }) } }) - .put( - "/:projectId", - apiKeyMiddleware, - zValidator("json", abbyConfigSchema), - async (c) => { - try { - const config = c.req.valid("json"); + .put('/:projectId', apiKeyMiddleware, zValidator('json', abbyConfigSchema), async (c) => { + try { + const config = c.req.valid('json') - await ConfigService.handlePUT({ - config: config, - projectId: c.req.param("projectId"), - userId: c.get("apiKey").userId, - }); + await ConfigService.handlePUT({ + config: config, + projectId: c.req.param('projectId'), + userId: c.get('apiKey').userId, + }) - return c.json({ message: "Config updated" }); - } catch (error) { - console.error(error); - return c.json({ error: "Could not Update Config" }, { status: 500 }); - } + return c.json({ message: 'Config updated' }) + } catch (error) { + console.error(error) + return c.json({ error: 'Could not Update Config' }, { status: 500 }) } - ); + }) } diff --git a/apps/web/src/api/routes/v1_event.test.ts b/apps/web/src/api/routes/v1_event.test.ts index 65b76318..fbe30708 100644 --- a/apps/web/src/api/routes/v1_event.test.ts +++ b/apps/web/src/api/routes/v1_event.test.ts @@ -1,16 +1,16 @@ -import { testClient } from "hono/testing"; -import { makeEventRoute } from "./v1_event"; -import { AbbyEventType } from "@tryabby/core"; -import { prisma } from "server/db/client"; -import { redis } from "server/db/redis"; +import { testClient } from 'hono/testing' +import { makeEventRoute } from './v1_event' +import { AbbyEventType } from '@tryabby/core' +import { prisma } from 'server/db/client' +import { redis } from 'server/db/redis' -vi.mock("server/common/plans", () => ({ +vi.mock('server/common/plans', () => ({ getLimitByPlan: vi.fn(() => {}), -})); +})) -vi.mock("../../env/client.mjs", () => ({})); +vi.mock('../../env/client.mjs', () => ({})) -vi.mock("server/db/client", () => ({ +vi.mock('server/db/client', () => ({ prisma: { event: { create: vi.fn(), @@ -19,67 +19,67 @@ vi.mock("server/db/client", () => ({ create: vi.fn(), }, }, -})); +})) -vi.mock("server/db/redis", () => ({ +vi.mock('server/db/redis', () => ({ redis: { - incr: vi.fn(async () => "test"), + incr: vi.fn(async () => 'test'), }, -})); +})) afterEach(() => { - vi.resetAllMocks(); -}); + vi.resetAllMocks() +}) -it("should work with correct PING events", async () => { - const app = makeEventRoute(); +it('should work with correct PING events', async () => { + const app = makeEventRoute() const res = await testClient(app).index.$post({ json: { - projectId: "test", - selectedVariant: "test", - testName: "test", + projectId: 'test', + selectedVariant: 'test', + testName: 'test', type: AbbyEventType.PING, }, - }); + }) - expect(prisma.event.create).toHaveBeenCalledTimes(1); - expect(prisma.apiRequest.create).toHaveBeenCalledTimes(1); - expect(redis.incr).toHaveBeenCalledTimes(1); -}); + expect(prisma.event.create).toHaveBeenCalledTimes(1) + expect(prisma.apiRequest.create).toHaveBeenCalledTimes(1) + expect(redis.incr).toHaveBeenCalledTimes(1) +}) -it("should work with correct ACT events", async () => { - const app = makeEventRoute(); +it('should work with correct ACT events', async () => { + const app = makeEventRoute() const res = await testClient(app).index.$post({ json: { - projectId: "test", - selectedVariant: "test", - testName: "test", + projectId: 'test', + selectedVariant: 'test', + testName: 'test', type: AbbyEventType.ACT, }, - }); + }) - expect(res.status).toEqual(200); - expect(prisma.event.create).toHaveBeenCalledTimes(1); - expect(prisma.apiRequest.create).toHaveBeenCalledTimes(1); - expect(redis.incr).toHaveBeenCalledTimes(1); -}); + expect(res.status).toEqual(200) + expect(prisma.event.create).toHaveBeenCalledTimes(1) + expect(prisma.apiRequest.create).toHaveBeenCalledTimes(1) + expect(redis.incr).toHaveBeenCalledTimes(1) +}) -it("should ignore unknown events", async () => { - const app = makeEventRoute(); +it('should ignore unknown events', async () => { + const app = makeEventRoute() const res = await testClient(app).index.$post({ json: { - projectId: "test", - selectedVariant: "test", - testName: "test", + projectId: 'test', + selectedVariant: 'test', + testName: 'test', type: 3 as any, }, - }); + }) - expect(res.status).toBeGreaterThanOrEqual(400); - expect(prisma.event.create).toHaveBeenCalledTimes(0); - expect(prisma.apiRequest.create).toHaveBeenCalledTimes(0); - expect(redis.incr).toHaveBeenCalledTimes(0); -}); + expect(res.status).toBeGreaterThanOrEqual(400) + expect(prisma.event.create).toHaveBeenCalledTimes(0) + expect(prisma.apiRequest.create).toHaveBeenCalledTimes(0) + expect(redis.incr).toHaveBeenCalledTimes(0) +}) diff --git a/apps/web/src/api/routes/v1_event.ts b/apps/web/src/api/routes/v1_event.ts index 8b343449..638ae8d5 100644 --- a/apps/web/src/api/routes/v1_event.ts +++ b/apps/web/src/api/routes/v1_event.ts @@ -1,85 +1,78 @@ -import { zValidator } from "@hono/zod-validator"; -import { abbyEventSchema, AbbyEventType } from "@tryabby/core"; -import { Ratelimit } from "@upstash/ratelimit"; -import { Redis } from "@upstash/redis"; -import { Hono } from "hono"; +import { zValidator } from '@hono/zod-validator' +import { abbyEventSchema, AbbyEventType } from '@tryabby/core' +import { Ratelimit } from '@upstash/ratelimit' +import { Redis } from '@upstash/redis' +import { Hono } from 'hono' -import isbot from "isbot"; -import { EventService } from "server/services/EventService"; -import { RequestCache } from "server/services/RequestCache"; -import { RequestService } from "server/services/RequestService"; +import isbot from 'isbot' +import { EventService } from 'server/services/EventService' +import { RequestCache } from 'server/services/RequestCache' +import { RequestService } from 'server/services/RequestService' export function makeEventRoute() { - const app = new Hono().post( - "/", - zValidator("json", abbyEventSchema), - async (c) => { - const now = performance.now(); - // filter out bot users - if (isbot(c.req.header("user-agent"))) { - return c.text("Forbidden", 403); - } + const app = new Hono().post('/', zValidator('json', abbyEventSchema), async (c) => { + const now = performance.now() + // filter out bot users + if (isbot(c.req.header('user-agent'))) { + return c.text('Forbidden', 403) + } - const event = c.req.valid("json"); + const event = c.req.valid('json') - try { - // // we only want to rate limit in production - if (process.env.NODE_ENV === "production") { - // Create a new ratelimiter, that allows 10 requests per 10 seconds - const ratelimit = new Ratelimit({ - redis: Redis.fromEnv(), - limiter: Ratelimit.slidingWindow(10, "10 s"), - }); + try { + // // we only want to rate limit in production + if (process.env.NODE_ENV === 'production') { + // Create a new ratelimiter, that allows 10 requests per 10 seconds + const ratelimit = new Ratelimit({ + redis: Redis.fromEnv(), + limiter: Ratelimit.slidingWindow(10, '10 s'), + }) - const clientIp = (c.req.header("x-forwarded-for") || "") - .split(",") - .pop() - ?.trim(); + const clientIp = (c.req.header('x-forwarded-for') || '').split(',').pop()?.trim() - if (!clientIp) { - console.error("Unable to get client IP"); + if (!clientIp) { + console.error('Unable to get client IP') - return c.text("Internal Server Error", 500); - } + return c.text('Internal Server Error', 500) + } - const { success } = await ratelimit.limit(clientIp); + const { success } = await ratelimit.limit(clientIp) - if (!success) { - return c.text("Too Many Requests", 429); - } + if (!success) { + return c.text('Too Many Requests', 429) } + } - const duration = performance.now() - now; + const duration = performance.now() - now - // TODO: add those to a queue and process them in a background job as they are not critical - switch (event.type) { - case AbbyEventType.PING: - case AbbyEventType.ACT: { - await EventService.createEvent(event); - break; - } - default: { - event.type satisfies never; - } + // TODO: add those to a queue and process them in a background job as they are not critical + switch (event.type) { + case AbbyEventType.PING: + case AbbyEventType.ACT: { + await EventService.createEvent(event) + break } + default: { + event.type satisfies never + } + } - await RequestCache.increment(event.projectId); - await RequestService.storeRequest({ - projectId: event.projectId, - type: "TRACK_VIEW", - durationInMs: duration, - apiVersion: "V1", - }).catch((e) => { - console.error("Unable to store request", e); - }); + await RequestCache.increment(event.projectId) + await RequestService.storeRequest({ + projectId: event.projectId, + type: 'TRACK_VIEW', + durationInMs: duration, + apiVersion: 'V1', + }).catch((e) => { + console.error('Unable to store request', e) + }) - return c.text("OK", 200); - } catch (err) { - console.error(err); - return c.text("Internal Server Error", 500); - } + return c.text('OK', 200) + } catch (err) { + console.error(err) + return c.text('Internal Server Error', 500) } - ); + }) - return app; + return app } diff --git a/apps/web/src/api/routes/v1_project_data.test.ts b/apps/web/src/api/routes/v1_project_data.test.ts index 158cbeb0..3bdcd5e4 100644 --- a/apps/web/src/api/routes/v1_project_data.test.ts +++ b/apps/web/src/api/routes/v1_project_data.test.ts @@ -1,52 +1,52 @@ -import { testClient } from "hono/testing"; -import { makeProjectDataRoute } from "./v1_project_data"; -import { jobManager } from "server/queue/Manager"; -import { FeatureFlag, FeatureFlagValue, Option, Test } from "@prisma/client"; -import { Decimal } from "@prisma/client/runtime/library"; +import { testClient } from 'hono/testing' +import { makeProjectDataRoute } from './v1_project_data' +import { jobManager } from 'server/queue/Manager' +import { FeatureFlag, FeatureFlagValue, Option, Test } from '@prisma/client' +import { Decimal } from '@prisma/client/runtime/library' -vi.mock("../../env/server.mjs", () => ({ +vi.mock('../../env/server.mjs', () => ({ env: {}, -})); +})) -vi.mock("server/queue/Manager", () => ({ +vi.mock('server/queue/Manager', () => ({ jobManager: { emit: vi.fn().mockResolvedValue(null), }, -})); +})) -vi.mock("server/db/client", () => ({ +vi.mock('server/db/client', () => ({ prisma: { featureFlagValue: { findMany: vi.fn().mockResolvedValue([ { - environmentId: "", + environmentId: '', flag: { - name: "First Flag", - type: "BOOLEAN", + name: 'First Flag', + type: 'BOOLEAN', }, - flagId: "", - id: "", - value: "true", + flagId: '', + id: '', + value: 'true', }, { - environmentId: "", + environmentId: '', flag: { - name: "First Config", - type: "NUMBER", + name: 'First Config', + type: 'NUMBER', }, - flagId: "", - id: "", - value: "2", + flagId: '', + id: '', + value: '2', }, - ] satisfies Array }>), + ] satisfies Array }>), }, test: { findMany: vi.fn().mockResolvedValue([ { - id: "", - name: "First Test", + id: '', + name: 'First Test', createdAt: new Date(), - projectId: "", + projectId: '', updatedAt: new Date(), options: [ { @@ -65,88 +65,88 @@ vi.mock("server/db/client", () => ({ }, ] satisfies Array< Test & { - options: Array>; + options: Array> } >), }, }, -})); +})) -vi.mock("server/db/redis", () => ({ +vi.mock('server/db/redis', () => ({ redis: { get: vi.fn(async () => {}), incr: vi.fn(async () => {}), }, -})); +})) afterEach(() => { - vi.clearAllMocks(); -}); + vi.clearAllMocks() +}) -describe("Get Config", () => { - it("should return the correct config", async () => { - const app = makeProjectDataRoute(); +describe('Get Config', () => { + it('should return the correct config', async () => { + const app = makeProjectDataRoute() - const res = await testClient(app)[":projectId"].$get({ + const res = await testClient(app)[':projectId'].$get({ param: { - projectId: "test", + projectId: 'test', }, query: { - environment: "test", + environment: 'test', }, - }); - expect(res.status).toBe(200); - const data = await res.json(); + }) + expect(res.status).toBe(200) + const data = await res.json() // typeguard to make test fail if data is not AbbyDataResponse - if ("error" in data) { - throw new Error("Expected data to not have an error key"); + if ('error' in data) { + throw new Error('Expected data to not have an error key') } - expect((data as any).error).toBeUndefined(); + expect((data as any).error).toBeUndefined() - expect(data.tests).toHaveLength(1); - expect(data.tests?.[0]?.name).toBe("First Test"); - expect(data.tests?.[0]?.weights).toEqual([0.25, 0.25, 0.25, 0.25]); + expect(data.tests).toHaveLength(1) + expect(data.tests?.[0]?.name).toBe('First Test') + expect(data.tests?.[0]?.weights).toEqual([0.25, 0.25, 0.25, 0.25]) - expect(data.flags).toHaveLength(1); - expect(data.flags?.[0]?.name).toBe("First Flag"); - expect(data.flags?.[0]?.value).toBe(true); + expect(data.flags).toHaveLength(1) + expect(data.flags?.[0]?.name).toBe('First Flag') + expect(data.flags?.[0]?.value).toBe(true) - expect(data.remoteConfig).toHaveLength(1); - expect(data.remoteConfig?.[0]?.name).toBe("First Config"); - expect(data.remoteConfig?.[0]?.value).toBe(2); + expect(data.remoteConfig).toHaveLength(1) + expect(data.remoteConfig?.[0]?.name).toBe('First Config') + expect(data.remoteConfig?.[0]?.value).toBe(2) - expect(vi.mocked(jobManager.emit)).toHaveBeenCalledTimes(1); + expect(vi.mocked(jobManager.emit)).toHaveBeenCalledTimes(1) expect(vi.mocked(jobManager.emit)).toHaveBeenCalledWith( - "after-data-request", + 'after-data-request', expect.objectContaining({}) - ); - }); -}); + ) + }) +}) -describe("Get Config Script", () => { - it("should return the correct config script", async () => { - const app = makeProjectDataRoute(); +describe('Get Config Script', () => { + it('should return the correct config script', async () => { + const app = makeProjectDataRoute() - const res = await testClient(app)[":projectId"]["script.js"].$get({ + const res = await testClient(app)[':projectId']['script.js'].$get({ param: { - projectId: "test", + projectId: 'test', }, query: { - environment: "test", + environment: 'test', }, - }); - expect(res.status).toBe(200); - const data = await res.text(); + }) + expect(res.status).toBe(200) + const data = await res.text() expect(data).toMatchInlineSnapshot( '"window.__abby_data__ = {\\"tests\\":[{\\"name\\":\\"First Test\\",\\"weights\\":[0.25,0.25,0.25,0.25]}],\\"flags\\":[{\\"name\\":\\"First Flag\\",\\"value\\":true}],\\"remoteConfig\\":[{\\"name\\":\\"First Config\\",\\"value\\":2}]}"' - ); + ) - expect(vi.mocked(jobManager.emit)).toHaveBeenCalledTimes(1); + expect(vi.mocked(jobManager.emit)).toHaveBeenCalledTimes(1) expect(vi.mocked(jobManager.emit)).toHaveBeenCalledWith( - "after-data-request", + 'after-data-request', expect.objectContaining({}) - ); - }); -}); + ) + }) +}) diff --git a/apps/web/src/api/routes/v1_project_data.ts b/apps/web/src/api/routes/v1_project_data.ts index 2733ca7c..95474f93 100644 --- a/apps/web/src/api/routes/v1_project_data.ts +++ b/apps/web/src/api/routes/v1_project_data.ts @@ -1,40 +1,40 @@ -import { Context, Hono } from "hono"; -import { timing, startTime, endTime } from "hono/timing"; -import { cors } from "hono/cors"; -import { zValidator } from "@hono/zod-validator"; -import { prisma } from "server/db/client"; -import { ABBY_WINDOW_KEY, AbbyDataResponse } from "@tryabby/core"; -import { z } from "zod"; -import createCache from "server/common/memory-cache"; -import { transformFlagValue } from "lib/flags"; -import { jobManager } from "server/queue/Manager"; - -export const X_ABBY_CACHE_HEADER = "X-Abby-Cache"; +import { Context, Hono } from 'hono' +import { timing, startTime, endTime } from 'hono/timing' +import { cors } from 'hono/cors' +import { zValidator } from '@hono/zod-validator' +import { prisma } from 'server/db/client' +import { ABBY_WINDOW_KEY, AbbyDataResponse } from '@tryabby/core' +import { z } from 'zod' +import createCache from 'server/common/memory-cache' +import { transformFlagValue } from 'lib/flags' +import { jobManager } from 'server/queue/Manager' + +export const X_ABBY_CACHE_HEADER = 'X-Abby-Cache' const configCache = createCache({ - name: "configCache", + name: 'configCache', expireAfterMilliseconds: 1000 * 10, -}); +}) async function getAbbyResponseWithCache({ environment, projectId, c, }: { - environment: string; - projectId: string; - c: Context; + environment: string + projectId: string + c: Context }) { - startTime(c, "readCache"); - const cachedConfig = configCache.get(projectId + environment); - endTime(c, "readCache"); + startTime(c, 'readCache') + const cachedConfig = configCache.get(projectId + environment) + endTime(c, 'readCache') - c.header(X_ABBY_CACHE_HEADER, cachedConfig !== undefined ? "HIT" : "MISS"); + c.header(X_ABBY_CACHE_HEADER, cachedConfig !== undefined ? 'HIT' : 'MISS') if (cachedConfig) { - return cachedConfig; + return cachedConfig } - startTime(c, "db"); + startTime(c, 'db') const [tests, flags] = await Promise.all([ prisma.test.findMany({ where: { @@ -51,8 +51,8 @@ async function getAbbyResponseWithCache({ }, include: { flag: { select: { name: true, type: true } } }, }), - ]); - endTime(c, "db"); + ]) + endTime(c, 'db') const response = { tests: tests.map((test) => ({ @@ -60,122 +60,120 @@ async function getAbbyResponseWithCache({ weights: test.options.map((o) => o.chance.toNumber()), })), flags: flags - .filter(({ flag }) => flag.type === "BOOLEAN") + .filter(({ flag }) => flag.type === 'BOOLEAN') .map((flagValue) => { return { name: flagValue.flag.name, value: transformFlagValue(flagValue.value, flagValue.flag.type), - }; + } }), remoteConfig: flags - .filter(({ flag }) => flag.type !== "BOOLEAN") + .filter(({ flag }) => flag.type !== 'BOOLEAN') .map((flagValue) => { return { name: flagValue.flag.name, value: transformFlagValue(flagValue.value, flagValue.flag.type), - }; + } }), - } satisfies AbbyDataResponse; + } satisfies AbbyDataResponse - configCache.set(projectId + environment, response); - return response; + configCache.set(projectId + environment, response) + return response } export function makeProjectDataRoute() { const app = new Hono() .get( - "/:projectId", + '/:projectId', cors({ - origin: "*", + origin: '*', maxAge: 86400, }), zValidator( - "query", + 'query', z.object({ environment: z.string(), }) ), timing(), async (c) => { - const projectId = c.req.param("projectId"); - const { environment } = c.req.valid("query"); + const projectId = c.req.param('projectId') + const { environment } = c.req.valid('query') - const now = performance.now(); + const now = performance.now() try { - startTime(c, "getAbbyResponseWithCache"); + startTime(c, 'getAbbyResponseWithCache') const response = await getAbbyResponseWithCache({ projectId, environment, c, - }); - endTime(c, "getAbbyResponseWithCache"); + }) + endTime(c, 'getAbbyResponseWithCache') - const duration = performance.now() - now; + const duration = performance.now() - now - jobManager.emit("after-data-request", { - apiVersion: "V1", + jobManager.emit('after-data-request', { + apiVersion: 'V1', functionDuration: duration, projectId, - }); + }) - return c.json(response); + return c.json(response) } catch (e) { - console.error(e); - return c.json({ error: "Internal server error" }, { status: 500 }); + console.error(e) + return c.json({ error: 'Internal server error' }, { status: 500 }) } } ) .get( - "/:projectId/script.js", + '/:projectId/script.js', cors({ - origin: "*", + origin: '*', maxAge: 86400, }), zValidator( - "query", + 'query', z.object({ environment: z.string(), }) ), timing(), async (c) => { - const projectId = c.req.param("projectId"); - const { environment } = c.req.valid("query"); + const projectId = c.req.param('projectId') + const { environment } = c.req.valid('query') - const now = performance.now(); + const now = performance.now() try { - startTime(c, "getAbbyResponseWithCache"); + startTime(c, 'getAbbyResponseWithCache') const response = await getAbbyResponseWithCache({ projectId, environment, c, - }); - endTime(c, "getAbbyResponseWithCache"); + }) + endTime(c, 'getAbbyResponseWithCache') - const jsContent = `window.${ABBY_WINDOW_KEY} = ${JSON.stringify( - response - )}`; + const jsContent = `window.${ABBY_WINDOW_KEY} = ${JSON.stringify(response)}` - const duration = performance.now() - now; + const duration = performance.now() - now - jobManager.emit("after-data-request", { - apiVersion: "V1", + jobManager.emit('after-data-request', { + apiVersion: 'V1', functionDuration: duration, projectId, - }); + }) return c.text(jsContent, { headers: { - "Content-Type": "application/javascript", + 'Content-Type': 'application/javascript', }, - }); + }) } catch (e) { - console.error(e); - return c.json({ error: "Internal server error" }, { status: 500 }); + console.error(e) + return c.json({ error: 'Internal server error' }, { status: 500 }) } } - ); - return app; + ) + return app } diff --git a/apps/web/src/components/AddABTestModal.tsx b/apps/web/src/components/AddABTestModal.tsx index 488dcf04..ecdae09d 100644 --- a/apps/web/src/components/AddABTestModal.tsx +++ b/apps/web/src/components/AddABTestModal.tsx @@ -1,18 +1,15 @@ -import { TRPCClientError } from "@trpc/client"; -import { TRPC_ERROR_CODES_BY_KEY } from "@trpc/server/rpc"; - -import { useState } from "react"; -import { toast } from "react-hot-toast"; -import { PlausibleEvents } from "types/plausible-events"; -import { trpc } from "utils/trpc"; -import { Modal } from "./Modal"; -import { - CreateTestSection, - DEFAULT_NEW_VARIANT_PREFIX, -} from "./Test/CreateTestSection"; -import { useTracking } from "lib/tracking"; - -type UIVariant = { name: string; weight: number }; +import { TRPCClientError } from '@trpc/client' +import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc' + +import { useState } from 'react' +import { toast } from 'react-hot-toast' +import { PlausibleEvents } from 'types/plausible-events' +import { trpc } from 'utils/trpc' +import { Modal } from './Modal' +import { CreateTestSection, DEFAULT_NEW_VARIANT_PREFIX } from './Test/CreateTestSection' +import { useTracking } from 'lib/tracking' + +type UIVariant = { name: string; weight: number } const INITIAL_VARIANTS: Array = [ { @@ -22,44 +19,43 @@ const INITIAL_VARIANTS: Array = [ name: `${DEFAULT_NEW_VARIANT_PREFIX}2`, }, // give each variant a weight of 100 / number of variants -].map((v, _, array) => ({ ...v, weight: 100 / array.length })); +].map((v, _, array) => ({ ...v, weight: 100 / array.length })) -const INITIAL_TEST_NAME = "New Test"; +const INITIAL_TEST_NAME = 'New Test' type Props = { - onClose: () => void; - isOpen: boolean; - projectId: string; -}; + onClose: () => void + isOpen: boolean + projectId: string +} export const AddABTestModal = ({ onClose, isOpen, projectId }: Props) => { - const [testName, setTestName] = useState(INITIAL_TEST_NAME); + const [testName, setTestName] = useState(INITIAL_TEST_NAME) const [variants, setVariants] = - useState>(INITIAL_VARIANTS); + useState>(INITIAL_VARIANTS) const variantsIncludeDuplicates = - new Set(variants.map((variant) => variant.name)).size !== variants.length; + new Set(variants.map((variant) => variant.name)).size !== variants.length const variantsWeightSum = variants .map(({ weight }) => weight) - .reduce((sum, weight) => (sum += weight), 0); + .reduce((sum, weight) => (sum += weight), 0) - const isConfirmButtonDisabled = - variantsIncludeDuplicates || variantsWeightSum !== 100; + const isConfirmButtonDisabled = variantsIncludeDuplicates || variantsWeightSum !== 100 - const createTestMutation = trpc.tests.createTest.useMutation(); + const createTestMutation = trpc.tests.createTest.useMutation() - const trpcContext = trpc.useContext(); + const trpcContext = trpc.useContext() - const trackEvent = useTracking(); + const trackEvent = useTracking() const onCreateClick = async () => { try { - if (!variants.length || !variants[0]) throw new Error(); + if (!variants.length || !variants[0]) throw new Error() if (variants.reduce((acc, curr) => acc + curr.weight, 0) !== 100) { - toast.error("Weights must add up to 100"); - return; + toast.error('Weights must add up to 100') + return } await createTestMutation.mutateAsync({ @@ -69,38 +65,37 @@ export const AddABTestModal = ({ onClose, isOpen, projectId }: Props) => { weight: v.weight / 100, })), projectId: projectId, - }); + }) trpcContext.project.getProjectData.invalidate({ projectId: projectId, - }); + }) - setTestName(INITIAL_TEST_NAME); - setVariants(INITIAL_VARIANTS); + setTestName(INITIAL_TEST_NAME) + setVariants(INITIAL_VARIANTS) - onClose(); - trackEvent("AB-Test Created", { - props: { "Amount Of Variants": variants.length }, - }); - toast.success("Test created"); + onClose() + trackEvent('AB-Test Created', { + props: { 'Amount Of Variants': variants.length }, + }) + toast.success('Test created') } catch (e) { toast.error( - e instanceof TRPCClientError && - e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN + e instanceof TRPCClientError && e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN ? e.message - : "Could not create test" - ); + : 'Could not create test' + ) } - }; + } return ( @@ -111,5 +106,5 @@ export const AddABTestModal = ({ onClose, isOpen, projectId }: Props) => { variants={variants} /> - ); -}; + ) +} diff --git a/apps/web/src/components/AddFeatureFlagModal.tsx b/apps/web/src/components/AddFeatureFlagModal.tsx index 7c1dd212..390b5e6f 100644 --- a/apps/web/src/components/AddFeatureFlagModal.tsx +++ b/apps/web/src/components/AddFeatureFlagModal.tsx @@ -1,33 +1,33 @@ -import { FeatureFlagType } from "@prisma/client"; -import { TRPCClientError } from "@trpc/client"; -import { TRPC_ERROR_CODES_BY_KEY } from "@trpc/server/rpc"; -import { getFlagTypeClassName, transformDBFlagTypeToclient } from "lib/flags"; -import { cn } from "lib/utils"; -import { useRef, useState } from "react"; -import { toast } from "react-hot-toast"; -import { trpc } from "utils/trpc"; -import { FlagIcon } from "./FlagIcon"; -import { JSONEditor } from "./JSONEditor"; -import { Modal } from "./Modal"; -import { RadioSelect } from "./RadioSelect"; - -import { Toggle } from "./Toggle"; - -import { useTracking } from "lib/tracking"; -import { Input } from "./ui/input"; +import { FeatureFlagType } from '@prisma/client' +import { TRPCClientError } from '@trpc/client' +import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc' +import { getFlagTypeClassName, transformDBFlagTypeToclient } from 'lib/flags' +import { cn } from 'lib/utils' +import { useRef, useState } from 'react' +import { toast } from 'react-hot-toast' +import { trpc } from 'utils/trpc' +import { FlagIcon } from './FlagIcon' +import { JSONEditor } from './JSONEditor' +import { Modal } from './Modal' +import { RadioSelect } from './RadioSelect' + +import { Toggle } from './Toggle' + +import { useTracking } from 'lib/tracking' +import { Input } from './ui/input' type Props = { - onClose: () => void; - isOpen: boolean; - projectId: string; - isRemoteConfig?: boolean; -}; + onClose: () => void + isOpen: boolean + projectId: string + isRemoteConfig?: boolean +} export type FlagFormValues = { - name: string; - value: string; - type: FeatureFlagType; -}; + name: string + value: string + type: FeatureFlagType +} export function ChangeFlagForm({ initialValues, @@ -36,175 +36,152 @@ export function ChangeFlagForm({ canChangeType = true, isRemoteConfig, }: { - initialValues: FlagFormValues; - onChange: (values: FlagFormValues) => void; - errors: Partial; - canChangeType?: boolean; - isRemoteConfig?: boolean; + initialValues: FlagFormValues + onChange: (values: FlagFormValues) => void + errors: Partial + canChangeType?: boolean + isRemoteConfig?: boolean }) { - const inputRef = useRef(null); + const inputRef = useRef(null) - const [state, setState] = useState(initialValues); + const [state, setState] = useState(initialValues) const valueRef = useRef>({ - [FeatureFlagType.BOOLEAN]: "false", - [FeatureFlagType.STRING]: "", - [FeatureFlagType.NUMBER]: "", - [FeatureFlagType.JSON]: "", - }); + [FeatureFlagType.BOOLEAN]: 'false', + [FeatureFlagType.STRING]: '', + [FeatureFlagType.NUMBER]: '', + [FeatureFlagType.JSON]: '', + }) const onChange = (values: Partial) => { - const newState = { ...state, ...values }; + const newState = { ...state, ...values } // if type changed, save the value if (values.type != null && values.type !== state.type) { - valueRef.current[state.type] = state.value; - newState.value = valueRef.current[newState.type] ?? ""; + valueRef.current[state.type] = state.value + newState.value = valueRef.current[newState.type] ?? '' } - setState(newState); - onChangeHandler(newState); - }; + setState(newState) + onChangeHandler(newState) + } return ( -
+
- + onChange({ name: e.target.value })} - placeholder={isRemoteConfig ? "My Remote Config" : "My Feature Flag"} + placeholder={isRemoteConfig ? 'My Remote Config' : 'My Feature Flag'} /> - {errors.name && ( -

{errors.name}

- )} + {errors.name &&

{errors.name}

}
{isRemoteConfig && (
- + isRemoteConfig && flagType !== "BOOLEAN" - ) + .filter(([, flagType]) => isRemoteConfig && flagType !== 'BOOLEAN') .map(([key, flagType]) => ({ label: ( -
- +
+ {transformDBFlagTypeToclient(flagType)}
), value: flagType, }))} onChange={(value) => { - if (!canChangeType) return; + if (!canChangeType) return onChange({ type: value, - value: value === "BOOLEAN" ? "false" : "", - }); + value: value === 'BOOLEAN' ? 'false' : '', + }) }} initialValue={initialValues.type} />
)}
- - {state.type === "BOOLEAN" && ( + + {state.type === 'BOOLEAN' && ( onChange({ value: String(newState) })} - label={state.value === "true" ? "Enabled" : "Disabled"} + label={state.value === 'true' ? 'Enabled' : 'Disabled'} /> )} - {state.type === "STRING" && ( + {state.type === 'STRING' && ( onChange({ value: e.target.value })} - placeholder={ - isRemoteConfig ? "My Remote Config" : "My Feature Flag" - } + placeholder={isRemoteConfig ? 'My Remote Config' : 'My Feature Flag'} /> )} - {state.type === "NUMBER" && ( + {state.type === 'NUMBER' && ( onChange({ value: e.target.value })} onKeyDown={(e) => { // prevent e, E, +, - - ["e", "E", "+", "-"].includes(e.key) && e.preventDefault(); + ;['e', 'E', '+', '-'].includes(e.key) && e.preventDefault() }} - placeholder="123" - /> - )} - {state.type === "JSON" && ( - onChange({ value: e })} + placeholder='123' /> )} - {errors.value && ( -

{errors.value}

+ {state.type === 'JSON' && ( + onChange({ value: e })} /> )} + {errors.value &&

{errors.value}

}
- ); + ) } -export const AddFeatureFlagModal = ({ - onClose, - isOpen, - projectId, - isRemoteConfig, -}: Props) => { - const inputRef = useRef(null); - const ctx = trpc.useContext(); - const stateRef = useRef(); - const trackEvent = useTracking(); +export const AddFeatureFlagModal = ({ onClose, isOpen, projectId, isRemoteConfig }: Props) => { + const inputRef = useRef(null) + const ctx = trpc.useContext() + const stateRef = useRef() + const trackEvent = useTracking() - const [errors, setErrors] = useState>({}); + const [errors, setErrors] = useState>({}) const { mutateAsync } = trpc.flags.addFlag.useMutation({ onSuccess() { - ctx.flags.getFlags.invalidate({ projectId }); + ctx.flags.getFlags.invalidate({ projectId }) }, - }); + }) return ( { - const errors: Partial = {}; - if (!stateRef.current) return; + const errors: Partial = {} + if (!stateRef.current) return - const trimmedName = stateRef.current.name.trim(); + const trimmedName = stateRef.current.name.trim() if (!trimmedName) { - errors.name = "Name is required"; + errors.name = 'Name is required' } if (!stateRef.current?.value) { - errors.value = "Value is required"; + errors.value = 'Value is required' } if (Object.keys(errors).length > 0) { - setErrors(errors); - return; + setErrors(errors) + return } try { @@ -212,33 +189,32 @@ export const AddFeatureFlagModal = ({ ...stateRef.current, name: trimmedName, projectId, - }); - toast.success("Flag created"); - trackEvent("Feature Flag Created", { - props: { "Feature Flag Type": stateRef.current.type }, - }); - onClose(); + }) + toast.success('Flag created') + trackEvent('Feature Flag Created', { + props: { 'Feature Flag Type': stateRef.current.type }, + }) + onClose() } catch (e) { toast.error( - e instanceof TRPCClientError && - e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN + e instanceof TRPCClientError && e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN ? e.message - : "Error creating flag" - ); + : 'Error creating flag' + ) } }} > (stateRef.current = newState)} canChangeType isRemoteConfig={isRemoteConfig} /> - ); -}; + ) +} diff --git a/apps/web/src/components/AsyncCodeExample.tsx b/apps/web/src/components/AsyncCodeExample.tsx index a1a3b06d..23fbf224 100644 --- a/apps/web/src/components/AsyncCodeExample.tsx +++ b/apps/web/src/components/AsyncCodeExample.tsx @@ -1,18 +1,18 @@ -import { trpc } from "utils/trpc"; -import { BaseCodeSnippet } from "./CodeSnippet"; -import { LoadingSpinner } from "./LoadingSpinner"; +import { trpc } from 'utils/trpc' +import { BaseCodeSnippet } from './CodeSnippet' +import { LoadingSpinner } from './LoadingSpinner' export function AsyncCodeExample() { - const { data, isLoading } = trpc.example.exampleSnippet.useQuery(); + const { data, isLoading } = trpc.example.exampleSnippet.useQuery() if (isLoading) { return ( -
- +
+
- ); + ) } - if (!data) return null; + if (!data) return null - return ; + return } diff --git a/apps/web/src/components/Avatar.tsx b/apps/web/src/components/Avatar.tsx index 2f5c6766..d2b50353 100644 --- a/apps/web/src/components/Avatar.tsx +++ b/apps/web/src/components/Avatar.tsx @@ -1,42 +1,42 @@ -import * as RadixAvatar from "@radix-ui/react-avatar"; -import { ComponentProps } from "react"; -import { twMerge } from "tailwind-merge"; +import * as RadixAvatar from '@radix-ui/react-avatar' +import { ComponentProps } from 'react' +import { twMerge } from 'tailwind-merge' function getNameFromEmail(email: string) { - const [name] = email.split("@"); - if (name?.includes(".")) { - const [firstName, lastName] = name.split("."); - return `${firstName} ${lastName}`; + const [name] = email.split('@') + if (name?.includes('.')) { + const [firstName, lastName] = name.split('.') + return `${firstName} ${lastName}` } - return name; + return name } type Props = { - imageUrl?: string; - userName?: string; -} & ComponentProps<(typeof RadixAvatar)["Root"]>; + imageUrl?: string + userName?: string +} & ComponentProps<(typeof RadixAvatar)['Root']> export const Avatar = ({ imageUrl, userName, className, ...props }: Props) => ( - {(userName?.includes("@") ? getNameFromEmail(userName) : userName) - ?.split(" ") + {(userName?.includes('@') ? getNameFromEmail(userName) : userName) + ?.split(' ') .map((name) => name[0])} -); +) diff --git a/apps/web/src/components/BlogLayout.tsx b/apps/web/src/components/BlogLayout.tsx index bb161d21..1a6049fc 100644 --- a/apps/web/src/components/BlogLayout.tsx +++ b/apps/web/src/components/BlogLayout.tsx @@ -1,14 +1,14 @@ -import type { PostMeta } from "pages/tips-and-insights"; -import { MarketingLayout, MarketingLayoutProps } from "./MarketingLayout"; -import dayjs from "dayjs"; -import Image from "next/image"; -import { SignupButton } from "./SignupButton"; -import { Divider } from "./Divider"; -import { NextSeo } from "next-seo"; +import type { PostMeta } from 'pages/tips-and-insights' +import { MarketingLayout, MarketingLayoutProps } from './MarketingLayout' +import dayjs from 'dayjs' +import Image from 'next/image' +import { SignupButton } from './SignupButton' +import { Divider } from './Divider' +import { NextSeo } from 'next-seo' -type Props = Pick & { - meta: PostMeta; -}; +type Props = Pick & { + meta: PostMeta +} export function BlogLayout({ children, seoTitle, meta }: Props) { return ( @@ -20,44 +20,41 @@ export function BlogLayout({ children, seoTitle, meta }: Props) { url: `${ process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` - : "https://www.tryabby.com" + : 'https://www.tryabby.com' }${meta.imageUrl}`, }, ], }} /> - -
-

- Published on {dayjs(meta.publishedAt).format("MMMM DD, YYYY")} + +

+

+ Published on {dayjs(meta.publishedAt).format('MMMM DD, YYYY')}

-

{meta.title}

-
+

{meta.title}

+
{meta.title}
-
+
{children}
- -
- Abby is an Open Source SaaS for developers to streamline A/B testing - and feature flagging. + +
+ Abby is an Open Source SaaS for developers to streamline A/B testing and feature + flagging.
Make data-driven decisions and improve user experience with ease.
Made for developers, by developers. - +
- ); + ) } diff --git a/apps/web/src/components/Button.tsx b/apps/web/src/components/Button.tsx index f9ad580b..88ab2f38 100644 --- a/apps/web/src/components/Button.tsx +++ b/apps/web/src/components/Button.tsx @@ -1,31 +1,27 @@ -import React, { - type PropsWithChildren, - type ComponentPropsWithoutRef, -} from "react"; -import { twMerge } from "tailwind-merge"; +import React, { type PropsWithChildren, type ComponentPropsWithoutRef } from 'react' +import { twMerge } from 'tailwind-merge' interface ButtonProps { - as?: T; - children?: React.ReactNode; + as?: T + children?: React.ReactNode } -export function Button({ +export function Button({ children, className, as, ...props -}: ButtonProps & - Omit, keyof ButtonProps>) { - const Component = as || "button"; +}: ButtonProps & Omit, keyof ButtonProps>) { + const Component = as || 'button' return ( {children} - ); + ) } diff --git a/apps/web/src/components/CodeSnippet.tsx b/apps/web/src/components/CodeSnippet.tsx index 2a81b82c..3efee79d 100644 --- a/apps/web/src/components/CodeSnippet.tsx +++ b/apps/web/src/components/CodeSnippet.tsx @@ -1,77 +1,77 @@ -import clsx from "clsx"; -import { useState } from "react"; -import toast from "react-hot-toast"; -import { FaAngular, FaCopy, FaReact } from "react-icons/fa"; -import { TbBrandAngular, TbBrandSvelte } from "react-icons/tb"; -import { TbBrandNextjs } from "react-icons/tb"; -import type { CodeSnippetData, Integrations } from "utils/snippets"; -import { trpc } from "utils/trpc"; -import { LoadingSpinner } from "./LoadingSpinner"; -import { twMerge } from "tailwind-merge"; +import clsx from 'clsx' +import { useState } from 'react' +import toast from 'react-hot-toast' +import { FaAngular, FaCopy, FaReact } from 'react-icons/fa' +import { TbBrandAngular, TbBrandSvelte } from 'react-icons/tb' +import { TbBrandNextjs } from 'react-icons/tb' +import type { CodeSnippetData, Integrations } from 'utils/snippets' +import { trpc } from 'utils/trpc' +import { LoadingSpinner } from './LoadingSpinner' +import { twMerge } from 'tailwind-merge' const INTEGRATIONS: Record< Integrations, { - name: string; - icon: React.ComponentType<{ className?: string }>; + name: string + icon: React.ComponentType<{ className?: string }> } > = { nextjs: { - name: "Next.js", + name: 'Next.js', icon: TbBrandNextjs, }, react: { - name: "React", + name: 'React', icon: FaReact, }, svelte: { - name: "Svelte", + name: 'Svelte', icon: TbBrandSvelte, }, angular: { - name: "Angular", + name: 'Angular', icon: TbBrandAngular, }, -}; +} type Props = { - projectId: string; -}; + projectId: string +} export function BaseCodeSnippet( props: Record & { - className?: string; + className?: string } ) { const [currentIntegration, setCurrentIntegration] = useState( Object.keys(INTEGRATIONS)[0] as Integrations - ); + ) const onCopyClick = () => { - toast.success("Successfully copied code snippet!"); - navigator.clipboard.writeText(props[currentIntegration].code); - }; + toast.success('Successfully copied code snippet!') + navigator.clipboard.writeText(props[currentIntegration].code) + } return ( -
-
-
-
-
-
+
+
+
+
+
+
-
+
{Object.entries(INTEGRATIONS).map(([key, { icon: Icon, name }]) => (
setCurrentIntegration(key as Integrations)} className={clsx( - "flex cursor-pointer items-center px-3 py-2 font-semibold text-white transition-colors ease-in-out hover:bg-gray-900", - key === currentIntegration && "bg-gray-900" + 'flex cursor-pointer items-center px-3 py-2 font-semibold text-white transition-colors ease-in-out hover:bg-gray-900', + key === currentIntegration && 'bg-gray-900' )} > - + {name}
))} @@ -79,34 +79,32 @@ export function BaseCodeSnippet(
-
+
- ); + ) } export function CodeSnippet({ projectId }: Props) { const { data, isLoading } = trpc.project.getCodeSnippet.useQuery({ projectId, - }); + }) if (isLoading) { return ( -
- +
+
- ); + ) } - if (!data) return null; + if (!data) return null - return ; + return } diff --git a/apps/web/src/components/CodeSnippetModalButton.tsx b/apps/web/src/components/CodeSnippetModalButton.tsx index f118d813..8cfed4d9 100644 --- a/apps/web/src/components/CodeSnippetModalButton.tsx +++ b/apps/web/src/components/CodeSnippetModalButton.tsx @@ -1,57 +1,57 @@ -import { Dialog } from "@headlessui/react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { BsCodeSlash } from "react-icons/bs"; -import { CodeSnippet } from "./CodeSnippet"; +import { Dialog } from '@headlessui/react' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { BsCodeSlash } from 'react-icons/bs' +import { CodeSnippet } from './CodeSnippet' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "./DropdownMenu"; -import { Code, Copy } from "lucide-react"; -import { useProjectId } from "lib/hooks/useProjectId"; -import { toast } from "react-hot-toast"; -import { useTracking } from "lib/tracking"; -import { Button } from "./ui/button"; +} from './DropdownMenu' +import { Code, Copy } from 'lucide-react' +import { useProjectId } from 'lib/hooks/useProjectId' +import { toast } from 'react-hot-toast' +import { useTracking } from 'lib/tracking' +import { Button } from './ui/button' function CodeSnippetModal({ isOpen, onClose, }: { - isOpen: boolean; - onClose: () => void; + isOpen: boolean + onClose: () => void }) { - const projectId = useRouter().query.projectId as string; + const projectId = useRouter().query.projectId as string return ( - + {/* The backdrop, rendered as a fixed sibling to the panel container */} - - ); + ) } export function CodeSnippetModalButton() { - const trackEvent = useTracking(); - const projectId = useProjectId(); - const [isOpen, setIsOpen] = useState(false); + const trackEvent = useTracking() + const projectId = useProjectId() + const [isOpen, setIsOpen] = useState(false) const onCopyProjectId = async () => { toast.promise(navigator.clipboard.writeText(projectId), { - loading: "Copying to clipboard...", - error: "Failed to copy to clipboard", - success: "Copied Project ID to clipboard", - }); - }; + loading: 'Copying to clipboard...', + error: 'Failed to copy to clipboard', + success: 'Copied Project ID to clipboard', + }) + } return ( @@ -59,24 +59,24 @@ export function CodeSnippetModalButton() { asChild // all other events are prevented by radix :( onPointerDown={() => { - trackEvent("Dashboard Code Clicked"); + trackEvent('Dashboard Code Clicked') }} > - - + - + Copy Project ID setIsOpen(true)}> - + Generate Code Snippet setIsOpen(false)} /> - ); + ) } diff --git a/apps/web/src/components/CreateAPIKeyModal.tsx b/apps/web/src/components/CreateAPIKeyModal.tsx index 392a94ad..30bdef7b 100644 --- a/apps/web/src/components/CreateAPIKeyModal.tsx +++ b/apps/web/src/components/CreateAPIKeyModal.tsx @@ -1,62 +1,61 @@ -import { TRPCClientError } from "@trpc/client"; -import { TRPC_ERROR_CODES_BY_KEY } from "@trpc/server/rpc"; -import { useRef, useState } from "react"; -import { toast } from "react-hot-toast"; -import { trpc } from "utils/trpc"; -import { Modal } from "./Modal"; -import { Input } from "./ui/input"; +import { TRPCClientError } from '@trpc/client' +import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc' +import { useRef, useState } from 'react' +import { toast } from 'react-hot-toast' +import { trpc } from 'utils/trpc' +import { Modal } from './Modal' +import { Input } from './ui/input' type Props = { - onClose: () => void; - isOpen: boolean; - projectId: string; -}; + onClose: () => void + isOpen: boolean + projectId: string +} export const CreateAPIKeyModal = ({ onClose, isOpen, projectId }: Props) => { - const inputRef = useRef(null); - const ctx = trpc.useContext(); - const [name, setName] = useState(""); - const trimmedName = name.trim(); + const inputRef = useRef(null) + const ctx = trpc.useContext() + const [name, setName] = useState('') + const trimmedName = name.trim() const { mutate: createApiKey } = trpc.apikey.createApiKey.useMutation({ onSuccess() { - toast.success("API Key created"); + toast.success('API Key created') }, - }); + }) return ( { if (!trimmedName) { - toast.error("Name is required"); - return; + toast.error('Name is required') + return } try { - setName(""); - toast.success("API Key created"); - onClose(); + setName('') + toast.success('API Key created') + onClose() } catch (e) { toast.error( - e instanceof TRPCClientError && - e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN + e instanceof TRPCClientError && e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN ? e.message - : "Error creating API Key" - ); + : 'Error creating API Key' + ) } }} > - + setName(e.target.value)} - placeholder="Name of the Application" + placeholder='Name of the Application' /> - ); -}; + ) +} diff --git a/apps/web/src/components/CreateEnvironmentModal.tsx b/apps/web/src/components/CreateEnvironmentModal.tsx index 879244ab..9c2c591d 100644 --- a/apps/web/src/components/CreateEnvironmentModal.tsx +++ b/apps/web/src/components/CreateEnvironmentModal.tsx @@ -1,73 +1,68 @@ -import { TRPCClientError } from "@trpc/client"; -import { TRPC_ERROR_CODES_BY_KEY } from "@trpc/server/rpc"; -import { useRef, useState } from "react"; -import { toast } from "react-hot-toast"; -import { PlausibleEvents } from "types/plausible-events"; -import { trpc } from "utils/trpc"; -import { Modal } from "./Modal"; -import { useTracking } from "lib/tracking"; -import { Input } from "./ui/input"; +import { TRPCClientError } from '@trpc/client' +import { TRPC_ERROR_CODES_BY_KEY } from '@trpc/server/rpc' +import { useRef, useState } from 'react' +import { toast } from 'react-hot-toast' +import { PlausibleEvents } from 'types/plausible-events' +import { trpc } from 'utils/trpc' +import { Modal } from './Modal' +import { useTracking } from 'lib/tracking' +import { Input } from './ui/input' type Props = { - onClose: () => void; - isOpen: boolean; - projectId: string; -}; + onClose: () => void + isOpen: boolean + projectId: string +} -export const CreateEnvironmentModal = ({ - onClose, - isOpen, - projectId, -}: Props) => { - const inputRef = useRef(null); - const ctx = trpc.useContext(); - const [name, setName] = useState(""); - const trimmedName = name.trim(); +export const CreateEnvironmentModal = ({ onClose, isOpen, projectId }: Props) => { + const inputRef = useRef(null) + const ctx = trpc.useContext() + const [name, setName] = useState('') + const trimmedName = name.trim() const { mutateAsync } = trpc.environments.addEnvironment.useMutation({ onSuccess() { - ctx.flags.getFlags.invalidate({ projectId }); + ctx.flags.getFlags.invalidate({ projectId }) }, - }); - const trackEvent = useTracking(); + }) + const trackEvent = useTracking() return ( { if (!trimmedName) { - toast.error("Name is required"); - return; + toast.error('Name is required') + return } try { await mutateAsync({ name, projectId, - }); - setName(""); - toast.success("Environment created"); - trackEvent("Environment Created"); - onClose(); + }) + setName('') + toast.success('Environment created') + trackEvent('Environment Created') + onClose() } catch (e) { toast.error( - e instanceof TRPCClientError && - e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN + e instanceof TRPCClientError && e.shape.code === TRPC_ERROR_CODES_BY_KEY.FORBIDDEN ? e.message - : "Error creating environment" - ); + : 'Error creating environment' + ) } }} > - + setName(e.target.value)} - type="text" - placeholder="production" + type='text' + placeholder='production' /> - ); -}; + ) +} diff --git a/apps/web/src/components/CreateProjectModal.tsx b/apps/web/src/components/CreateProjectModal.tsx index 8e1f6d4f..339027d1 100644 --- a/apps/web/src/components/CreateProjectModal.tsx +++ b/apps/web/src/components/CreateProjectModal.tsx @@ -1,78 +1,70 @@ -import { useRouter } from "next/router"; -import { ChangeEvent, useRef, useState } from "react"; -import toast from "react-hot-toast"; -import { trpc } from "utils/trpc"; -import { Modal } from "./Modal"; -import { useSession } from "next-auth/react"; -import { useTracking } from "lib/tracking"; -import { Input } from "./ui/input"; +import { useRouter } from 'next/router' +import { ChangeEvent, useRef, useState } from 'react' +import toast from 'react-hot-toast' +import { trpc } from 'utils/trpc' +import { Modal } from './Modal' +import { useSession } from 'next-auth/react' +import { useTracking } from 'lib/tracking' +import { Input } from './ui/input' type Props = { - onClose: () => void; -}; + onClose: () => void +} export const CreateProjectModal = ({ onClose }: Props) => { - const router = useRouter(); - const projectName = useRef(""); - const [isValidName, setIsValidName] = useState(true); - const trpcContext = trpc.useContext(); - const session = useSession(); - const trackEvent = useTracking(); + const router = useRouter() + const projectName = useRef('') + const [isValidName, setIsValidName] = useState(true) + const trpcContext = trpc.useContext() + const session = useSession() + const trackEvent = useTracking() - const { mutateAsync: createProjectTRPC } = - trpc.project.createProject.useMutation({ - onSuccess() { - toast.success("Project created"); - trpcContext.project.getProjectData.invalidate(); - trpcContext.user.getProjects.invalidate(); - }, - }); + const { mutateAsync: createProjectTRPC } = trpc.project.createProject.useMutation({ + onSuccess() { + toast.success('Project created') + trpcContext.project.getProjectData.invalidate() + trpcContext.user.getProjects.invalidate() + }, + }) const createProject = async () => { if (projectName.current.length < 3) { - setIsValidName(false); - return; + setIsValidName(false) + return } const project = await createProjectTRPC({ projectName: projectName.current, - }); + }) - onClose(); - if (!project) return; + onClose() + if (!project) return await session.update({ projectIds: [...(session.data?.user?.projectIds ?? []), project.id], lastOpenProjectId: project.id, - }); - trackEvent("Project Created"); - router.push(`/projects/${project.id}`); - }; + }) + trackEvent('Project Created') + router.push(`/projects/${project.id}`) + } const handleChange = (e: ChangeEvent) => { - projectName.current = e.currentTarget.value; - setIsValidName(projectName.current.length >= 3); - }; + projectName.current = e.currentTarget.value + setIsValidName(projectName.current.length >= 3) + } return ( - + {!isValidName && ( -
- Please enter a name with atleast 3 characters{" "} -
+
Please enter a name with atleast 3 characters
)}
- ); -}; + ) +} diff --git a/apps/web/src/components/DashboardButton.tsx b/apps/web/src/components/DashboardButton.tsx index 9b2fbf2e..3bffa6d3 100644 --- a/apps/web/src/components/DashboardButton.tsx +++ b/apps/web/src/components/DashboardButton.tsx @@ -1,8 +1,8 @@ -import { ComponentPropsWithRef } from "react"; -import { Button } from "./ui/button"; +import { ComponentPropsWithRef } from 'react' +import { Button } from './ui/button' -type Props = ComponentPropsWithRef<"button">; +type Props = ComponentPropsWithRef<'button'> export function DashboardButton({ className, ...props }: Props) { - return - + Bold @@ -115,7 +123,7 @@ const MenuBar = ({ editor }: { editor: TipTapEditor | null }) => { - + Align Left @@ -162,9 +170,9 @@ const MenuBar = ({ editor }: { editor: TipTapEditor | null }) => {
- ); -}; + ) +} export const Editor = ({ className, content, onUpdate }: Props) => { const editor = useEditor({ @@ -230,37 +238,37 @@ export const Editor = ({ className, content, onUpdate }: Props) => { StarterKit.configure({ orderedList: { HTMLAttributes: { - class: "list-decimal list-outside", + class: 'list-decimal list-outside', }, }, bulletList: { HTMLAttributes: { - class: "list-disc list-outside", + class: 'list-disc list-outside', }, }, }), TextAlign.configure({ - types: ["heading", "paragraph"], + types: ['heading', 'paragraph'], }), ], editorProps: { attributes: { class: - "prose-invert prose-base leading-none my-5 focus:outline-none border-pink-50/60 border rounded-lg py-3 px-2 h-64 overflow-y-auto", + 'prose-invert prose-base leading-none my-5 focus:outline-none border-pink-50/60 border rounded-lg py-3 px-2 h-64 overflow-y-auto', }, }, content, onUpdate: ({ editor }) => { - onUpdate?.(editor.getHTML()); + onUpdate?.(editor.getHTML()) }, - }); + }) return ( <> -
+
- ); -}; + ) +} diff --git a/apps/web/src/components/Feature.tsx b/apps/web/src/components/Feature.tsx index 83bc8c81..e24c2af2 100644 --- a/apps/web/src/components/Feature.tsx +++ b/apps/web/src/components/Feature.tsx @@ -1,24 +1,22 @@ -import type { LucideIcon } from "lucide-react"; -import type { IconType } from "react-icons"; +import type { LucideIcon } from 'lucide-react' +import type { IconType } from 'react-icons' type Props = { - children: React.ReactNode; - title: string; - subtitle: string; - icon: IconType | LucideIcon; -}; + children: React.ReactNode + title: string + subtitle: string + icon: IconType | LucideIcon +} export function Feature({ children, icon: Icon, subtitle, title }: Props) { return ( -
-
+
+
-

{title}

-

- {subtitle} -

-

{children}

+

{title}

+

{subtitle}

+

{children}

- ); + ) } diff --git a/apps/web/src/components/FeatureFlag.tsx b/apps/web/src/components/FeatureFlag.tsx index a61e021e..7299fbd6 100644 --- a/apps/web/src/components/FeatureFlag.tsx +++ b/apps/web/src/components/FeatureFlag.tsx @@ -1,23 +1,23 @@ -import { Popover, PopoverContent, PopoverTrigger } from "components/ui/popover"; -import dayjs from "dayjs"; -import { FaHistory } from "react-icons/fa"; -import { match, P } from "ts-pattern"; - -import { FeatureFlagHistory, FeatureFlagType } from "@prisma/client"; -import { Tooltip, TooltipContent, TooltipTrigger } from "components/Tooltip"; -import relativeTime from "dayjs/plugin/relativeTime"; -import { useState } from "react"; -import { toast } from "react-hot-toast"; -import { RouterOutputs, trpc } from "utils/trpc"; -import { Avatar } from "./Avatar"; -import { LoadingSpinner } from "./LoadingSpinner"; -import { Modal } from "./Modal"; -import { Edit } from "lucide-react"; -import { ChangeFlagForm, FlagFormValues } from "./AddFeatureFlagModal"; -import { Switch } from "./ui/switch"; -import { cn } from "lib/utils"; - -dayjs.extend(relativeTime); +import { Popover, PopoverContent, PopoverTrigger } from 'components/ui/popover' +import dayjs from 'dayjs' +import { FaHistory } from 'react-icons/fa' +import { match, P } from 'ts-pattern' + +import { FeatureFlagHistory, FeatureFlagType } from '@prisma/client' +import { Tooltip, TooltipContent, TooltipTrigger } from 'components/Tooltip' +import relativeTime from 'dayjs/plugin/relativeTime' +import { useState } from 'react' +import { toast } from 'react-hot-toast' +import { RouterOutputs, trpc } from 'utils/trpc' +import { Avatar } from './Avatar' +import { LoadingSpinner } from './LoadingSpinner' +import { Modal } from './Modal' +import { Edit } from 'lucide-react' +import { ChangeFlagForm, FlagFormValues } from './AddFeatureFlagModal' +import { Switch } from './ui/switch' +import { cn } from 'lib/utils' + +dayjs.extend(relativeTime) const getHistoryEventDescription = (event: FeatureFlagHistory) => { return match(event) @@ -26,24 +26,24 @@ const getHistoryEventDescription = (event: FeatureFlagHistory) => { newValue: P.not(P.nullish), oldValue: null, }, - () => "created" as const + () => 'created' as const ) .with( { newValue: P.not(P.nullish), oldValue: P.not(P.nullish), }, - () => "updated" as const + () => 'updated' as const ) - .run(); -}; + .run() +} const HistoryButton = ({ flagValueId }: { flagValueId: string }) => { const { data, isLoading, refetch: loadHistory, - } = trpc.flags.getHistory.useQuery({ flagValueId }, { enabled: false }); + } = trpc.flags.getHistory.useQuery({ flagValueId }, { enabled: false }) return ( @@ -52,39 +52,31 @@ const HistoryButton = ({ flagValueId }: { flagValueId: string }) => { - - + {isLoading && } {data !== undefined && ( <> -

Edited {data.length} times

-
-
+

Edited {data.length} times

+
+
{data.map((history) => ( -
+
- {history.user.name ?? history.user.email}{" "} - {getHistoryEventDescription(history)} this flag{" "} + {history.user.name ?? history.user.email}{' '} + {getHistoryEventDescription(history)} this flag{' '} {dayjs(history.createdAt).fromNow()}
@@ -95,8 +87,8 @@ const HistoryButton = ({ flagValueId }: { flagValueId: string }) => { - ); -}; + ) +} const ConfirmUpdateModal = ({ isOpen, @@ -108,57 +100,55 @@ const ConfirmUpdateModal = ({ type, flagValueId, }: { - isOpen: boolean; - onClose: () => void; - currentValue: string; - type: FeatureFlagType; - description: string; - projectId: string; - flagName: string; - flagValueId: string; + isOpen: boolean + onClose: () => void + currentValue: string + type: FeatureFlagType + description: string + projectId: string + flagName: string + flagValueId: string }) => { const [state, setState] = useState({ name: flagName, value: currentValue, type, - }); - const trpcContext = trpc.useContext(); + }) + const trpcContext = trpc.useContext() const { mutate: updateFlag } = trpc.flags.updateFlag.useMutation({ onSuccess(_, { value, flagValueId }) { trpcContext.flags.getFlags.setData({ projectId, types: [] }, (prev) => { - if (!prev) return prev; + if (!prev) return prev const flagToUpdate = prev.flags.find((flag) => flag.values.some((value) => value.id === flagValueId) - ); - if (!flagToUpdate) return; + ) + if (!flagToUpdate) return - const valueToUpdate = flagToUpdate.values.find( - (value) => value.id === flagValueId - ); - if (!valueToUpdate) return; + const valueToUpdate = flagToUpdate.values.find((value) => value.id === flagValueId) + if (!valueToUpdate) return - valueToUpdate.value = value.toString(); - return prev; - }); + valueToUpdate.value = value.toString() + return prev + }) - trpcContext.flags.getFlags.invalidate({ projectId }); - onClose(); + trpcContext.flags.getFlags.invalidate({ projectId }) + onClose() }, onError() { - toast.error(`Failed to update ${type === "BOOLEAN" ? "flag" : "value"}`); + toast.error(`Failed to update ${type === 'BOOLEAN' ? 'flag' : 'value'}`) }, - }); + }) return ( updateFlag({ ...state, flagValueId })} isOpen={isOpen} onClose={onClose} - size="full" + size='full' > setState(newState)} errors={{}} canChangeType={false} - isRemoteConfig={type !== "BOOLEAN"} + isRemoteConfig={type !== 'BOOLEAN'} /> -

Description:

+

Description:

{!description ? ( - "No description provided" + 'No description provided' ) : (

)} - ); -}; + ) +} type Props = { - flag: RouterOutputs["flags"]["getFlags"]["flags"][number]; - projectId: string; - environmentName: string; - flagValueId: string; - type: FeatureFlagType; -}; - -export function FeatureFlag({ - flag, - projectId, - environmentName, - flagValueId, - type, -}: Props) { - const [isUpdateConfirmationModalOpen, setIsUpdateConfirmationModalOpen] = - useState(false); + flag: RouterOutputs['flags']['getFlags']['flags'][number] + projectId: string + environmentName: string + flagValueId: string + type: FeatureFlagType +} + +export function FeatureFlag({ flag, projectId, environmentName, flagValueId, type }: Props) { + const [isUpdateConfirmationModalOpen, setIsUpdateConfirmationModalOpen] = useState(false) - const currentFlagValue = flag.values.find((f) => f.id === flagValueId)?.value; + const currentFlagValue = flag.values.find((f) => f.id === flagValueId)?.value if (currentFlagValue == null) { - return null; + return null } return ( <> - -

+ +

{environmentName}

- {typeof currentFlagValue === "string" && - currentFlagValue.trim() === "" - ? "Empty String" + {typeof currentFlagValue === 'string' && currentFlagValue.trim() === '' + ? 'Empty String' : currentFlagValue}
-
+
@@ -238,12 +218,12 @@ export function FeatureFlag({ isOpen={isUpdateConfirmationModalOpen} onClose={() => setIsUpdateConfirmationModalOpen(false)} flagValueId={flagValueId} - description={flag.description ?? ""} + description={flag.description ?? ''} projectId={projectId} flagName={flag.name} type={flag.type} currentValue={currentFlagValue} /> - ); + ) } diff --git a/apps/web/src/components/FlagIcon.tsx b/apps/web/src/components/FlagIcon.tsx index bf7fea4c..591abbea 100644 --- a/apps/web/src/components/FlagIcon.tsx +++ b/apps/web/src/components/FlagIcon.tsx @@ -1,18 +1,12 @@ -import { FeatureFlagType } from "@prisma/client"; -import { - Baseline, - Hash, - LucideProps, - ToggleLeft, - CurlyBraces, -} from "lucide-react"; -import { match } from "ts-pattern"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./Tooltip"; +import { FeatureFlagType } from '@prisma/client' +import { Baseline, Hash, LucideProps, ToggleLeft, CurlyBraces } from 'lucide-react' +import { match } from 'ts-pattern' +import { Tooltip, TooltipContent, TooltipTrigger } from './Tooltip' type Props = { - type: FeatureFlagType; - className?: string; -}; + type: FeatureFlagType + className?: string +} export function FlagIcon({ type, ...iconProps }: Props) { return ( @@ -20,18 +14,18 @@ export function FlagIcon({ type, ...iconProps }: Props) { - This Flag has the type {type} + This Flag has the type {type} {match(type) - .with("BOOLEAN", () => ) - .with("NUMBER", () => ) - .with("STRING", () => ) - .with("JSON", () => ) + .with('BOOLEAN', () => ) + .with('NUMBER', () => ) + .with('STRING', () => ) + .with('JSON', () => ) .exhaustive()} - ); + ) } diff --git a/apps/web/src/components/FlagPage.tsx b/apps/web/src/components/FlagPage.tsx index b4d45c84..f47072fc 100644 --- a/apps/web/src/components/FlagPage.tsx +++ b/apps/web/src/components/FlagPage.tsx @@ -1,28 +1,28 @@ -import { FeatureFlagType } from "@prisma/client"; -import { InferQueryResult } from "@trpc/react-query/dist/utils/inferReactQueryProcedure"; -import { AddFeatureFlagModal } from "components/AddFeatureFlagModal"; -import { CreateEnvironmentModal } from "components/CreateEnvironmentModal"; +import { FeatureFlagType } from '@prisma/client' +import { InferQueryResult } from '@trpc/react-query/dist/utils/inferReactQueryProcedure' +import { AddFeatureFlagModal } from 'components/AddFeatureFlagModal' +import { CreateEnvironmentModal } from 'components/CreateEnvironmentModal' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "components/DropdownMenu"; -import { Editor } from "components/Editor"; -import { FeatureFlag } from "components/FeatureFlag"; -import { Input } from "components/ui/input"; -import { Modal } from "components/Modal"; -import { Tooltip, TooltipContent, TooltipTrigger } from "components/Tooltip"; -import { useProjectId } from "lib/hooks/useProjectId"; -import { EditIcon, FileEditIcon, TrashIcon } from "lucide-react"; -import { useState } from "react"; -import { toast } from "react-hot-toast"; -import { AiOutlinePlus } from "react-icons/ai"; -import { BiInfoCircle } from "react-icons/bi"; -import { BsThreeDotsVertical } from "react-icons/bs"; -import { appRouter } from "server/trpc/router/_app"; -import { trpc } from "utils/trpc"; -import { Button } from "./ui/button"; +} from 'components/DropdownMenu' +import { Editor } from 'components/Editor' +import { FeatureFlag } from 'components/FeatureFlag' +import { Input } from 'components/ui/input' +import { Modal } from 'components/Modal' +import { Tooltip, TooltipContent, TooltipTrigger } from 'components/Tooltip' +import { useProjectId } from 'lib/hooks/useProjectId' +import { EditIcon, FileEditIcon, TrashIcon } from 'lucide-react' +import { useState } from 'react' +import { toast } from 'react-hot-toast' +import { AiOutlinePlus } from 'react-icons/ai' +import { BiInfoCircle } from 'react-icons/bi' +import { BsThreeDotsVertical } from 'react-icons/bs' +import { appRouter } from 'server/trpc/router/_app' +import { trpc } from 'utils/trpc' +import { Button } from './ui/button' const EditTitleModal = ({ flagId, @@ -30,40 +30,36 @@ const EditTitleModal = ({ onClose, title, }: { - isOpen: boolean; - onClose: () => void; - title: string; - flagId: string; + isOpen: boolean + onClose: () => void + title: string + flagId: string }) => { - const [newTitle, setNewTitle] = useState(title); - const trpcContext = trpc.useContext(); + const [newTitle, setNewTitle] = useState(title) + const trpcContext = trpc.useContext() const { mutate: updateTitle } = trpc.flags.updateFlagTitle.useMutation({ onSuccess() { - toast.success("Successfully updated the title"); - trpcContext.flags.getFlags.invalidate(); - onClose(); + toast.success('Successfully updated the title') + trpcContext.flags.getFlags.invalidate() + onClose() }, onError() { - toast.error("Failed to update the title"); + toast.error('Failed to update the title') }, - }); + }) return ( updateTitle({ flagId, title: newTitle })} isOpen={isOpen} onClose={onClose} - subtitle="The title is used to identify the flag in the UI." + subtitle='The title is used to identify the flag in the UI.' > - setNewTitle(e.target.value)} - placeholder="MyFlag" - /> + setNewTitle(e.target.value)} placeholder='MyFlag' /> - ); -}; + ) +} const EditDescriptionModal = ({ isOpen, @@ -71,44 +67,37 @@ const EditDescriptionModal = ({ flagId, description, }: { - isOpen: boolean; - onClose: () => void; - flagId: string; - description: string; + isOpen: boolean + onClose: () => void + flagId: string + description: string }) => { - const [newDescription, setNewDescription] = useState(description); - const trpcContext = trpc.useContext(); - const { mutate: updateDescription } = - trpc.flags.updateDescription.useMutation({ - onSuccess() { - toast.success("Successfully updated description"); - trpcContext.flags.getFlags.invalidate(); - onClose(); - }, - onError() { - toast.error("Failed to update description"); - }, - }); + const [newDescription, setNewDescription] = useState(description) + const trpcContext = trpc.useContext() + const { mutate: updateDescription } = trpc.flags.updateDescription.useMutation({ + onSuccess() { + toast.success('Successfully updated description') + trpcContext.flags.getFlags.invalidate() + onClose() + }, + onError() { + toast.error('Failed to update description') + }, + }) return ( - updateDescription({ flagId, description: newDescription }) - } - size="full" + title='Update Description' + confirmText='Save' + onConfirm={() => updateDescription({ flagId, description: newDescription })} + size='full' isOpen={isOpen} onClose={onClose} > - + - ); -}; + ) +} const DeleteFlagModal = ({ isOpen, @@ -117,28 +106,28 @@ const DeleteFlagModal = ({ flagName, type, }: { - isOpen: boolean; - onClose: () => void; - projectId: string; - flagName: string; - type: FeatureFlagType; + isOpen: boolean + onClose: () => void + projectId: string + flagName: string + type: FeatureFlagType }) => { - const trpcContext = trpc.useContext(); + const trpcContext = trpc.useContext() const { mutate: deleteFlag } = trpc.flags.removeFlag.useMutation({ onSuccess() { - toast.success(`Deleted ${type === "BOOLEAN" ? "flag" : "config"}`); - trpcContext.flags.getFlags.invalidate(); - onClose(); + toast.success(`Deleted ${type === 'BOOLEAN' ? 'flag' : 'config'}`) + trpcContext.flags.getFlags.invalidate() + onClose() }, onError() { - toast.error(`Failed to delete ${type === "BOOLEAN" ? "flag" : "config"}`); + toast.error(`Failed to delete ${type === 'BOOLEAN' ? 'flag' : 'config'}`) }, - }); + }) return ( deleteFlag({ name: flagName, projectId })} isOpen={isOpen} onClose={onClose} @@ -147,45 +136,37 @@ const DeleteFlagModal = ({ Are you sure that you want to delete the Flag {flagName}?

- ); -}; + ) +} export const FeatureFlagPageContent = ({ data, type, }: { - data: NonNullable< - InferQueryResult<(typeof appRouter)["flags"]["getFlags"]>["data"] - >; - type: "Flags" | "Remote Config"; + data: NonNullable['data']> + type: 'Flags' | 'Remote Config' }) => { - const [isCreateFlagModalOpen, setIsCreateFlagModalOpen] = useState(false); + const [isCreateFlagModalOpen, setIsCreateFlagModalOpen] = useState(false) const [activeFlagInfo, setActiveFlagInfo] = useState<{ - id: string; - action: "editDescription" | "editName" | "delete"; - } | null>(null); + id: string + action: 'editDescription' | 'editName' | 'delete' + } | null>(null) - const [isCreateEnvironmentModalOpen, setIsCreateEnvironmentModalOpen] = - useState(false); + const [isCreateEnvironmentModalOpen, setIsCreateEnvironmentModalOpen] = useState(false) - const projectId = useProjectId(); + const projectId = useProjectId() - const activeFlag = data.flags.find((flag) => flag.id === activeFlagInfo?.id); + const activeFlag = data.flags.find((flag) => flag.id === activeFlagInfo?.id) if (data.environments.length === 0) return ( -
-

- You don't have any environments set up! -

+
+

You don't have any environments set up!

- You need to have at least one environment to set up{" "} - {type === "Flags" ? "feature flags" : "remote config"} + You need to have at least one environment to set up{' '} + {type === 'Flags' ? 'feature flags' : 'remote config'}

-
- ); + ) return ( <>
-
+
setIsCreateFlagModalOpen(false)} projectId={projectId} - isRemoteConfig={type === "Remote Config"} + isRemoteConfig={type === 'Remote Config'} />
-
+
{data.flags.map((currentFlag) => { return ( -
-
-
-

- {currentFlag.name} -

+
+
+
+

{currentFlag.name}

@@ -248,18 +223,16 @@ export const FeatureFlagPageContent = ({ -

Description:

+

Description:

No description

", + __html: currentFlag.description ?? '

No description

', }} />
@@ -269,52 +242,49 @@ export const FeatureFlagPageContent = ({ - + { setActiveFlagInfo({ id: currentFlag.id, - action: "editName", - }); + action: 'editName', + }) }} > - + Edit Name { setActiveFlagInfo({ id: currentFlag.id, - action: "editDescription", - }); + action: 'editDescription', + }) }} > - + Edit Description { setActiveFlagInfo({ id: currentFlag.id, - action: "delete", - }); + action: 'delete', + }) }} > - - Delete {type === "Flags" ? "Flag" : "Config"} + + Delete {type === 'Flags' ? 'Flag' : 'Config'}
-
+
{currentFlag.values - .sort( - (a, b) => - a.environment.sortIndex - b.environment.sortIndex - ) + .sort((a, b) => a.environment.sortIndex - b.environment.sortIndex) .map((flagValue) => (
- ); + ) })}
@@ -336,26 +306,26 @@ export const FeatureFlagPageContent = ({ setActiveFlagInfo(null)} /> setActiveFlagInfo(null)} /> setActiveFlagInfo(null)} type={activeFlag.type} /> )} - ); -}; + ) +} diff --git a/apps/web/src/components/Footer.tsx b/apps/web/src/components/Footer.tsx index 302f3dc1..871392a6 100644 --- a/apps/web/src/components/Footer.tsx +++ b/apps/web/src/components/Footer.tsx @@ -1,31 +1,27 @@ -import { Github } from "lucide-react"; -import { BsDiscord, BsLinkedin } from "react-icons/bs"; -import { RiTwitterXLine } from "react-icons/ri"; -import Link from "next/link"; -import { DOCS_URL } from "@tryabby/core"; +import { Github } from 'lucide-react' +import { BsDiscord, BsLinkedin } from 'react-icons/bs' +import { RiTwitterXLine } from 'react-icons/ri' +import Link from 'next/link' +import { DOCS_URL } from '@tryabby/core' -const GITHUB_URL = "https://github.com/tryabby/abby"; -const LINKEDIN_URL = "https://www.linkedin.com/company/tryabby/"; -const TWITTER_URL = "https://twitter.com/tryabby"; -export const DISCORD_INVITE_URL = "https://discord.gg/nk7wKf7Pv2"; +const GITHUB_URL = 'https://github.com/tryabby/abby' +const LINKEDIN_URL = 'https://www.linkedin.com/company/tryabby/' +const TWITTER_URL = 'https://twitter.com/tryabby' +export const DISCORD_INVITE_URL = 'https://discord.gg/nk7wKf7Pv2' export function Footer() { return ( -