diff --git a/.env.example b/.env.example index e6b55e6..83ddd81 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,7 @@ VITE_REPORTING_NPUB= VITE_FALLBACK_MOD_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png # if there's no image, or if the image breaks somewhere down the line, then it should default to this image -VITE_FALLBACK_GAME_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png \ No newline at end of file +VITE_FALLBACK_GAME_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png + +# A comma separated list of npubs, this list is used to fetch just the posts from the admin +VITE_BLOG_NPUBS= diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index 981e1be..b2ba757 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -27,6 +27,8 @@ jobs: echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env + echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env + echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env cat .env - name: Create Build diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 10e4bc4..683d4d1 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -27,6 +27,8 @@ jobs: echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env + echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env + echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env cat .env - name: Create Build diff --git a/.github/workflows/release-pages-production.yaml b/.github/workflows/release-pages-production.yaml index a80541b..93cdf1e 100644 --- a/.github/workflows/release-pages-production.yaml +++ b/.github/workflows/release-pages-production.yaml @@ -35,6 +35,8 @@ jobs: echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env + echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env + echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env cat .env - name: Build run: npm run build diff --git a/package-lock.json b/package-lock.json index 2666d9c..19b9ff3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,22 @@ { "name": "degmods.com", - "version": "0.0.0", + "version": "0.0.0-alpha-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "degmods.com", - "version": "0.0.0", + "version": "0.0.0-alpha-1", "dependencies": { "@getalby/lightning-tools": "5.0.3", "@nostr-dev-kit/ndk": "2.10.0", "@nostr-dev-kit/ndk-cache-dexie": "2.5.1", "@reduxjs/toolkit": "2.2.6", - "@tiptap/core": "2.6.6", - "@tiptap/extension-link": "2.6.6", - "@tiptap/react": "2.6.6", - "@tiptap/starter-kit": "2.6.6", + "@tiptap/core": "2.9.1", + "@tiptap/extension-image": "^2.9.1", + "@tiptap/extension-link": "2.9.1", + "@tiptap/react": "2.9.1", + "@tiptap/starter-kit": "2.9.1", "@types/react-helmet": "^6.1.11", "axios": "1.7.3", "bech32": "2.0.0", @@ -26,6 +27,7 @@ "file-saver": "2.0.5", "fslightbox-react": "1.7.6", "lodash": "4.17.21", + "marked": "^14.1.3", "nostr-login": "1.5.2", "nostr-tools": "2.7.1", "papaparse": "5.4.1", @@ -39,6 +41,7 @@ "react-toastify": "10.0.5", "react-window": "1.8.10", "swiper": "11.1.11", + "turndown": "^7.2.0", "uuid": "10.0.0", "webln": "0.3.2" }, @@ -51,6 +54,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-window": "1.8.8", + "@types/turndown": "^5.0.5", "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", @@ -1038,6 +1042,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@noble/ciphers": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", @@ -1152,6 +1162,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1181,9 +1192,10 @@ } }, "node_modules/@remirror/core-constants": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz", - "integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" }, "node_modules/@remix-run/router": { "version": "1.17.1", @@ -1479,45 +1491,49 @@ } }, "node_modules/@tiptap/core": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.6.6.tgz", - "integrity": "sha512-VO5qTsjt6rwworkuo0s5AqYMfDA0ZwiTiH6FHKFSu2G/6sS7HKcc/LjPq+5Legzps4QYdBDl3W28wGsGuS1GdQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.9.1.tgz", + "integrity": "sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^2.6.6" + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-blockquote": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.6.6.tgz", - "integrity": "sha512-hAdsNlMfzzxld154hJqPqtWqO5i4/7HoDfuxmyqBxdMJ+e2UMaIGBGwoLRXG0V9UoRwJusjqlpyD7pIorxNlgA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.9.1.tgz", + "integrity": "sha512-Y0jZxc/pdkvcsftmEZFyG+73um8xrx6/DMfgUcNg3JAM63CISedNcr+OEI11L0oFk1KFT7/aQ9996GM6Kubdqg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-bold": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.6.6.tgz", - "integrity": "sha512-CD6gBhdQtCoqYSmx8oAV8gvKtVOGZSyyvuNYo7by9eZ56DqLYnd7kbUj0RH7o9Ymf/iJTOUJ6XcvrsWwo4lubg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.9.1.tgz", + "integrity": "sha512-e2P1zGpnnt4+TyxTC5pX/lPxPasZcuHCYXY0iwQ3bf8qRQQEjDfj3X7EI+cXqILtnhOiviEOcYmeu5op2WhQDg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.6.6.tgz", - "integrity": "sha512-IkfmlZq67aaegym5sBddBc/xXWCArxn5WJEl1oxKEayjQhybKSaqI7tk0lOx/x7fa5Ml1WlGpCFh+KKXbQTG0g==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.9.1.tgz", + "integrity": "sha512-DWUF6NG08/bZDWw0jCeotSTvpkyqZTi4meJPomG9Wzs/Ol7mEwlNCsCViD999g0+IjyXFatBk4DfUq1YDDu++Q==", + "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, @@ -1526,76 +1542,82 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.6.6.tgz", - "integrity": "sha512-WEKxbVSYuvmX2wkHWP8HXk5nzA7stYwtdaubwWH/R17kGI3IGScJuMQ9sEN82uzJU8bfgL9yCbH2bY8Fj/Q4Ow==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.9.1.tgz", + "integrity": "sha512-0hizL/0j9PragJObjAWUVSuGhN1jKjCFnhLQVRxtx4HutcvS/lhoWMvFg6ZF8xqWgIa06n6A7MaknQkqhTdhKA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-code": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.6.6.tgz", - "integrity": "sha512-JrEFKsZiLvfvOFhOnnrpA0TzCuJjDeysfbMeuKUZNV4+DhYOL28d39H1++rEtJAX0LcbBU60oC5/PrlU9SpvRQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.9.1.tgz", + "integrity": "sha512-WQqcVGe7i/E+yO3wz5XQteU1ETNZ00euUEl4ylVVmH2NM4Dh0KDjEhbhHlCM0iCfLUo7jhjC7dmS+hMdPUb+Tg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-code-block": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.6.6.tgz", - "integrity": "sha512-1YLp/zHMHSkE2xzht8nPR6T4sQJJ3ket798czxWuQEbetFv/l0U/mpiPpYSLObj6oTAoqYZ0kWXZj5eQSpPB8Q==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.9.1.tgz", + "integrity": "sha512-A/50wPWDqEUUUPhrwRKILP5gXMO5UlQ0F6uBRGYB9CEVOREam9yIgvONOnZVJtszHqOayjIVMXbH/JMBeq11/g==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-document": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.6.6.tgz", - "integrity": "sha512-6qlH5VWzLHHRVeeciRC6C4ZHpMsAGPNG16EF53z0GeMSaaFD/zU3B239QlmqXmLsAl8bpf8Bn93N0t2ABUvScw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.9.1.tgz", + "integrity": "sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.6.6.tgz", - "integrity": "sha512-O6CeKriA9uyHsg7Ui4z5ZjEWXQxrIL+1zDekffW0wenGC3G4LUsCzAiFS4LSrR9a3u7tnwqGApW10rdkmCGF4w==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.9.1.tgz", + "integrity": "sha512-wJZspSmJRkDBtPkzFz1g7gvZOEOayk8s93UHsgbJxcV4VWHYleZ5XhT74sZunSjefNDm3qC6v2BSgLp3vNHVKQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.6.6.tgz", - "integrity": "sha512-lPkESOfAUxgmXRiNqUU23WSyja5FUfSWjsW4hqe+BKNjsUt1OuFMEtYJtNc+MCGhhtPfFvM3Jg6g9jd6g5XsLQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.9.1.tgz", + "integrity": "sha512-MxZ7acNNsoNaKpetxfwi3Z11Bgrh0T2EJlCV77v9N1vWK38+st3H1WJanmLbPNtc2ocvhHJrz+DjDz3CWxQ9rQ==", + "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" }, @@ -1604,89 +1626,109 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.6.6.tgz", - "integrity": "sha512-O2lQ2t0X0Vsbn3yLWxFFHrXY6C2N9Y6ZF/M7LWzpcDTUZeWuhoNkFE/1yOM0h6ZX1DO2A9hNIrKpi5Ny8yx+QA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.9.1.tgz", + "integrity": "sha512-jsRBmX01vr+5H02GljiHMo0n5H1vzoMLmFarxe0Yq2d2l9G/WV2VWX2XnGliqZAYWd1bI0phs7uLQIN3mxGQTw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.6.6.tgz", - "integrity": "sha512-bsUuyYBrMDEiudx1dOQSr9MzKv13m0xHWrOK+DYxuIDYJb5g+c9un5cK7Js+et/HEYYSPOoH/iTW6h+4I5YeUg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.9.1.tgz", + "integrity": "sha512-fCuaOD/b7nDjm47PZ58oanq7y4ccS2wjPh42Qm0B0yipu/1fmC8eS1SmaXmk28F89BLtuL6uOCtR1spe+lZtlQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-heading": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.6.6.tgz", - "integrity": "sha512-bgx9vptVFi5yFkIw1OI53J7+xJ71Or3SOe/Q8eSpZv53DlaKpL/TzKw8Z54t1PrI2rJ6H9vrLtkvixJvBZH1Ug==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.9.1.tgz", + "integrity": "sha512-SjZowzLixOFaCrV2cMaWi1mp8REK0zK1b3OcVx7bCZfVSmsOETJyrAIUpCKA8o60NwF7pwhBg0MN8oXlNKMeFw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-history": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.6.6.tgz", - "integrity": "sha512-tPTzAmPGqMX5Bd5H8lzRpmsaMvB9DvI5Dy2za/VQuFtxgXmDiFVgHRkRXIuluSkPTuANu84XBOQ0cBijqY8x4w==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.9.1.tgz", + "integrity": "sha512-wp9qR1NM+LpvyLZFmdNaAkDq0d4jDJ7z7Fz7icFQPu31NVxfQYO3IXNmvJDCNu8hFAbImpA5aG8MBuwzRo0H9w==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.6.6.tgz", - "integrity": "sha512-cFEfv7euDpuLSe8exY8buwxkreKBAZY9Hn3EetKhPcLQo+ut5Y24chZTxFyf9b+Y0wz3UhOhLTZSz7fTobLqBA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.9.1.tgz", + "integrity": "sha512-ydUhABeaBI1CoJp+/BBqPhXINfesp1qMNL/jiDcMsB66fsD4nOyphpAJT7FaRFZFtQVF06+nttBtFZVkITQVqg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.9.1.tgz", + "integrity": "sha512-aGqJnsuS8oagIhsx7wetm8jw4NEDsOV0OSx4FQ4VPlUqWlnzK0N+erFKKJmXTdAxL8PGzoPSlITFH63MV3eV3Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-italic": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.6.6.tgz", - "integrity": "sha512-t7ZPsXqa8nJZZ/6D0rQyZ/KsvzLaSihC6hBTjUQ77CeDGV9PhDWjIcBW4OrvwraJDBd12ETBeQ2CkULJOgH+lQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.9.1.tgz", + "integrity": "sha512-VkNA6Vz96+/+7uBlsgM7bDXXx4b62T1fDam/3UKifA72aD/fZckeWrbT7KrtdUbzuIniJSbA0lpTs5FY29+86Q==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-link": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.6.6.tgz", - "integrity": "sha512-NJSR5Yf/dI3do0+Mr6e6nkbxRQcqbL7NOPxo5Xw8VaKs2Oe8PX+c7hyqN3GZgn6uEbZdbVi1xjAniUokouwpFg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.9.1.tgz", + "integrity": "sha512-yG+e3e8cCCN9dZjX4ttEe3e2xhh58ryi3REJV4MdiEkOT9QF75Bl5pUbMIS4tQ8HkOr04QBFMHKM12kbSxg1BA==", + "license": "MIT", "dependencies": { "linkifyjs": "^4.1.0" }, @@ -1695,78 +1737,97 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" } }, "node_modules/@tiptap/extension-list-item": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.6.6.tgz", - "integrity": "sha512-k+oEzZu2cgVKqPqOP1HzASOKLpTEV9m7mRVPAbuaaX8mSyvIgD6f+JUx9PvgYv//D918wk98LMoRBFX53tDJ4w==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.9.1.tgz", + "integrity": "sha512-6O4NtYNR5N2Txi4AC0/4xMRJq9xd4+7ShxCZCDVL0WDVX37IhaqMO7LGQtA6MVlYyNaX4W1swfdJaqrJJ5HIUw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.6.6.tgz", - "integrity": "sha512-AJwyfLXIi7iUGnK5twJbwdVVpQyh7fU6OK75h1AwDztzsOcoPcxtffDlZvUOd4ZtwuyhkzYqVkeI0f+abTWZTw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.9.1.tgz", + "integrity": "sha512-6J9jtv1XP8dW7/JNSH/K4yiOABc92tBJtgCsgP8Ep4+fjfjdj4HbjS1oSPWpgItucF2Fp/VF8qg55HXhjxHjTw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.6.6.tgz", - "integrity": "sha512-fD/onCr16UQWx+/xEmuFC2MccZZ7J5u4YaENh8LMnAnBXf78iwU7CAcmuc9rfAEO3qiLoYGXgLKiHlh2ZfD4wA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.9.1.tgz", + "integrity": "sha512-JOmT0xd4gd3lIhLwrsjw8lV+ZFROKZdIxLi0Ia05XSu4RLrrvWj0zdKMSB+V87xOWfSB3Epo95zAvnPox5Q16A==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-strike": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.6.6.tgz", - "integrity": "sha512-Ze8KhGk+wzSJSJRl5fbhTI6AvPu2LmcHYeO3pMEH8u4gV5WTXfmKJVStEIAzkoqvwEQVWzXvy8nDgsFQHiojPg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.9.1.tgz", + "integrity": "sha512-V5aEXdML+YojlPhastcu7w4biDPwmzy/fWq0T2qjfu5Te/THcqDmGYVBKESBm5x6nBy5OLkanw2O+KHu2quDdg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/extension-text": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.6.6.tgz", - "integrity": "sha512-e84uILnRzNzcwK1DVQNpXVmBG1Cq3BJipTOIDl1LHifOok7MBjhI/X+/NR0bd3N2t6gmDTWi63+4GuJ5EeDmsg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.9.1.tgz", + "integrity": "sha512-3wo9uCrkLVLQFgbw2eFU37QAa1jq1/7oExa+FF/DVxdtHRS9E2rnUZ8s2hat/IWzvPUHXMwo3Zg2XfhoamQpCA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6" + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.9.1.tgz", + "integrity": "sha512-LAxc0SeeiPiAVBwksczeA7BJSZb6WtVpYhy5Esvy9K0mK5kttB4KxtnXWeQzMIJZQbza65yftGKfQlexf/Y7yg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" } }, "node_modules/@tiptap/pm": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.6.6.tgz", - "integrity": "sha512-56FGLPn3fwwUlIbLs+BO21bYfyqP9fKyZQbQyY0zWwA/AG2kOwoXaRn7FOVbjP6CylyWpFJnpRRmgn694QKHEg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.9.1.tgz", + "integrity": "sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==", + "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.2.1", "prosemirror-collab": "^1.3.1", - "prosemirror-commands": "^1.5.2", + "prosemirror-commands": "^1.6.0", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", @@ -1774,14 +1835,14 @@ "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.0", "prosemirror-menu": "^1.2.4", - "prosemirror-model": "^1.22.2", + "prosemirror-model": "^1.22.3", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.4.0", - "prosemirror-trailing-node": "^2.0.9", - "prosemirror-transform": "^1.9.0", - "prosemirror-view": "^1.33.9" + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.0", + "prosemirror-view": "^1.34.3" }, "funding": { "type": "github", @@ -1789,13 +1850,15 @@ } }, "node_modules/@tiptap/react": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.6.6.tgz", - "integrity": "sha512-AUmdb/J1O/vCO2b8LL68ctcZr9a3931BwX4fUUZ1kCrCA5lTj2xz0rjeAtpxEdzLnR+Z7q96vB7vf7bPYOUAew==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.9.1.tgz", + "integrity": "sha512-LQJ34ZPfXtJF36SZdcn4Fiwsl2WxZ9YRJI87OLnsjJ45O+gV/PfBzz/4ap+LF8LOS0AbbGhTTjBOelPoNm+aYA==", + "license": "MIT", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.6.6", - "@tiptap/extension-floating-menu": "^2.6.6", + "@tiptap/extension-bubble-menu": "^2.9.1", + "@tiptap/extension-floating-menu": "^2.9.1", "@types/use-sync-external-store": "^0.0.6", + "fast-deep-equal": "^3", "use-sync-external-store": "^1.2.2" }, "funding": { @@ -1803,8 +1866,8 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/pm": "^2.6.6", + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0" } @@ -1815,30 +1878,32 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" }, "node_modules/@tiptap/starter-kit": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.6.6.tgz", - "integrity": "sha512-zb9xIg3WjG9AsJoyWrfqx5SL9WH7/HTdkB79jFpWtOF/Kaigo7fHFmhs2FsXtJMJlcdMTO2xeRuCYHt5ozXlhg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.9.1.tgz", + "integrity": "sha512-nsw6UF/7wDpPfHRhtGOwkj1ipIEiWZS1VGw+c14K61vM1CNj0uQ4jogbHwHZqN1dlL5Hh+FCqUHDPxG6ECbijg==", + "license": "MIT", "dependencies": { - "@tiptap/core": "^2.6.6", - "@tiptap/extension-blockquote": "^2.6.6", - "@tiptap/extension-bold": "^2.6.6", - "@tiptap/extension-bullet-list": "^2.6.6", - "@tiptap/extension-code": "^2.6.6", - "@tiptap/extension-code-block": "^2.6.6", - "@tiptap/extension-document": "^2.6.6", - "@tiptap/extension-dropcursor": "^2.6.6", - "@tiptap/extension-gapcursor": "^2.6.6", - "@tiptap/extension-hard-break": "^2.6.6", - "@tiptap/extension-heading": "^2.6.6", - "@tiptap/extension-history": "^2.6.6", - "@tiptap/extension-horizontal-rule": "^2.6.6", - "@tiptap/extension-italic": "^2.6.6", - "@tiptap/extension-list-item": "^2.6.6", - "@tiptap/extension-ordered-list": "^2.6.6", - "@tiptap/extension-paragraph": "^2.6.6", - "@tiptap/extension-strike": "^2.6.6", - "@tiptap/extension-text": "^2.6.6", - "@tiptap/pm": "^2.6.6" + "@tiptap/core": "^2.9.1", + "@tiptap/extension-blockquote": "^2.9.1", + "@tiptap/extension-bold": "^2.9.1", + "@tiptap/extension-bullet-list": "^2.9.1", + "@tiptap/extension-code": "^2.9.1", + "@tiptap/extension-code-block": "^2.9.1", + "@tiptap/extension-document": "^2.9.1", + "@tiptap/extension-dropcursor": "^2.9.1", + "@tiptap/extension-gapcursor": "^2.9.1", + "@tiptap/extension-hard-break": "^2.9.1", + "@tiptap/extension-heading": "^2.9.1", + "@tiptap/extension-history": "^2.9.1", + "@tiptap/extension-horizontal-rule": "^2.9.1", + "@tiptap/extension-italic": "^2.9.1", + "@tiptap/extension-list-item": "^2.9.1", + "@tiptap/extension-ordered-list": "^2.9.1", + "@tiptap/extension-paragraph": "^2.9.1", + "@tiptap/extension-strike": "^2.9.1", + "@tiptap/extension-text": "^2.9.1", + "@tiptap/extension-text-style": "^2.9.1", + "@tiptap/pm": "^2.9.1" }, "funding": { "type": "github", @@ -2032,6 +2097,13 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, + "node_modules/@types/turndown": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz", + "integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -3193,8 +3265,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -3847,6 +3918,18 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/marked": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.3.tgz", + "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -4504,11 +4587,12 @@ } }, "node_modules/prosemirror-trailing-node": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz", - "integrity": "sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", "dependencies": { - "@remirror/core-constants": "^2.0.2", + "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { @@ -4521,6 +4605,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -4537,9 +4622,10 @@ } }, "node_modules/prosemirror-view": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.1.tgz", - "integrity": "sha512-KS2xmqrAM09h3SLu1S2pNO/ZoIP38qkTJ6KFd7+BeSfmX/ek0n5yOfGuiTZjFNTC8GOsEIUa1tHxt+2FMu3yWQ==", + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.35.0.tgz", + "integrity": "sha512-Umtbh22fmUlpZpRTiOVXA0PpdRZeYEeXQsLp51VfnMhjkJrqJ0n8APinIZrRAD5Jr3UxH8FnOaUqRylSuMsqHA==", + "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -5028,6 +5114,7 @@ "version": "6.3.7", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", "dependencies": { "@popperjs/core": "^2.9.0" } @@ -5149,6 +5236,15 @@ "resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz", "integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw==" }, + "node_modules/turndown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", + "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", diff --git a/package.json b/package.json index a3973cb..b356c8a 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,11 @@ "@nostr-dev-kit/ndk": "2.10.0", "@nostr-dev-kit/ndk-cache-dexie": "2.5.1", "@reduxjs/toolkit": "2.2.6", - "@tiptap/core": "2.6.6", - "@tiptap/extension-link": "2.6.6", - "@tiptap/react": "2.6.6", - "@tiptap/starter-kit": "2.6.6", + "@tiptap/core": "2.9.1", + "@tiptap/extension-image": "^2.9.1", + "@tiptap/extension-link": "2.9.1", + "@tiptap/react": "2.9.1", + "@tiptap/starter-kit": "2.9.1", "@types/react-helmet": "^6.1.11", "axios": "1.7.3", "bech32": "2.0.0", @@ -28,6 +29,7 @@ "file-saver": "2.0.5", "fslightbox-react": "1.7.6", "lodash": "4.17.21", + "marked": "^14.1.3", "nostr-login": "1.5.2", "nostr-tools": "2.7.1", "papaparse": "5.4.1", @@ -41,6 +43,7 @@ "react-toastify": "10.0.5", "react-window": "1.8.10", "swiper": "11.1.11", + "turndown": "^7.2.0", "uuid": "10.0.0", "webln": "0.3.2" }, @@ -53,6 +56,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-window": "1.8.8", + "@types/turndown": "^5.0.5", "@types/uuid": "10.0.0", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", diff --git a/src/App.tsx b/src/App.tsx index fe0ccd1..95183ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,12 @@ import { RouterProvider } from 'react-router-dom' import { useEffect } from 'react' -import { router } from 'routes' +import { routerWithNdkContext } from 'routes' +import { useNDKContext } from 'hooks' import './styles/styles.css' function App() { + const ndkContext = useNDKContext() + useEffect(() => { // Find the element with id 'root' const rootElement = document.getElementById('root') @@ -21,7 +24,7 @@ function App() { } }, []) - return + return } export default App diff --git a/src/assets/games/Games_Steam2.csv b/src/assets/games/Games_Steam2.csv index 2e36989..ea4ddfe 100644 --- a/src/assets/games/Games_Steam2.csv +++ b/src/assets/games/Games_Steam2.csv @@ -38405,7 +38405,7 @@ Trap Legend Theme Song,, Gemini Strategy Origin,, Brain Marmelade,, Forsaken Flesh,, -"Warhammer 40,000: Darktide",, +"Warhammer 40,000: Darktide",,https://image.nostr.build/0c405bdb168c05f21f54b3bda39852eb2f9fa851068367eccd69f2bc0526a600.jpg Cleo - a pirate's tale,, Teenage Blob: Paperperson - The First Single,, Necronator: Dead Wrong - Special Commander Skin,, diff --git a/src/assets/games/Games_Steam3.csv b/src/assets/games/Games_Steam3.csv index 1328ca2..9592d26 100644 --- a/src/assets/games/Games_Steam3.csv +++ b/src/assets/games/Games_Steam3.csv @@ -45067,7 +45067,7 @@ Rooftop Postgirl Demo,, Mad Experiments 2: Premium Pack,, Arise from Shadows Demo,, Hentai Heaven's Slutty Salvation,, -Fields of Mistria,, +Fields of Mistria,,https://image.nostr.build/deb8fb380cdf2f42750f115141d762de791bf49e0af97ec733390360a1a5ddbf.jpg Maskonauts: Chat'Attack Soundtrack,, Nameless - The Departed Cycle,, Grave-Queen,, diff --git a/src/assets/img/DEGM Thumb.png b/src/assets/img/DEGM Thumb.png index 763a04b..6dd67f7 100644 Binary files a/src/assets/img/DEGM Thumb.png and b/src/assets/img/DEGM Thumb.png differ diff --git a/src/components/BlogCard.tsx b/src/components/BlogCard.tsx index e2065b7..e2d52a9 100644 --- a/src/components/BlogCard.tsx +++ b/src/components/BlogCard.tsx @@ -1,37 +1,33 @@ +import { Link } from 'react-router-dom' +import { BlogCardDetails } from 'types' +import { getBlogPageRoute } from 'routes' import '../styles/cardBlogs.css' +import placeholder from '../assets/img/DEGMods Placeholder Img.png' -type BlogCardProps = { - backgroundLink: string -} +type BlogCardProps = Partial + +export const BlogCard = ({ title, image, nsfw, naddr }: BlogCardProps) => { + if (!naddr) return null -export const BlogCard = ({ backgroundLink }: BlogCardProps) => { return ( - +
-
-

- This is a blog title, the best blog title in the world! -

+
+

{title}

+ {nsfw && ( +
+

NSFW

+
+ )}
-
{' '} -
+
+ ) } diff --git a/src/components/Inputs.tsx b/src/components/Inputs.tsx index e40cc91..38d48af 100644 --- a/src/components/Inputs.tsx +++ b/src/components/Inputs.tsx @@ -1,4 +1,5 @@ import Link from '@tiptap/extension-link' +import Image from '@tiptap/extension-image' import { Editor, EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import React, { useEffect } from 'react' @@ -127,7 +128,15 @@ type RichTextEditorProps = { const RichTextEditor = ({ content, updateContent }: RichTextEditorProps) => { const editor = useEditor({ - extensions: [StarterKit, Link], + extensions: [ + StarterKit, + Link, + Image.configure({ + HTMLAttributes: { + class: 'IBMSMSMBSSPostImg' + } + }) + ], onUpdate: ({ editor }) => { // Update the state when the editor content changes updateContent(editor.getHTML()) @@ -158,7 +167,7 @@ type MenuBarProps = { editor: Editor } -const MenuBar = ({ editor }: MenuBarProps) => { +export const MenuBar = ({ editor }: MenuBarProps) => { const setLink = () => { // Prompt the user to enter a URL let url = prompt('URL') @@ -181,6 +190,17 @@ const MenuBar = ({ editor }: MenuBarProps) => { const unsetLink = () => editor.chain().focus().unsetLink().run() + const setImage = () => { + let url = prompt('URL') + if (url) { + if (!/^(http|https):\/\//i.test(url)) { + url = `https://${url}` + } + return editor.chain().focus().setImage({ src: url }).run() + } + return false + } + const buttons: MenuBarButtonProps[] = [ { label: 'Bold', @@ -211,7 +231,7 @@ const MenuBar = ({ editor }: MenuBarProps) => { { label: 'Paragraph', isActive: editor.isActive('paragraph'), - onClick: () => editor.chain().focus().toggleStrike().run() + onClick: () => editor.chain().focus().setParagraph().run() }, // eslint-disable-next-line @typescript-eslint/no-explicit-any ...[1, 2, 3, 4, 5, 6].map((level: any) => ({ @@ -244,6 +264,11 @@ const MenuBar = ({ editor }: MenuBarProps) => { isActive: editor.isActive('link'), onClick: editor.isActive('link') ? unsetLink : setLink }, + { + label: 'Image', + isActive: editor.isActive('image'), + onClick: setImage + }, { label: 'Horizontal rule', onClick: () => editor.chain().focus().setHorizontalRule().run() @@ -298,7 +323,55 @@ const MenuBarButton = ({ onClick={onClick} disabled={disabled} className={`btn btnMain btnMainTipTap ${isActive ? 'is-active' : ''}`} + type='button' > {label} ) + +interface InputFieldUncontrolledProps extends React.ComponentProps<'input'> { + label: string + description?: string + error?: string +} +/** + * Uncontrolled input component with design classes, label, description and error support + * + * Extends {@link React.ComponentProps<'input'> React.ComponentProps<'input'>} + * @param label + * @param description + * @param error + * + * @see {@link React.ComponentProps<'input'>} + */ +export const InputFieldUncontrolled = ({ + label, + description, + error, + ...rest +}: InputFieldUncontrolledProps) => ( +
+ + {description &&

{description}

} + + {error && } +
+) + +interface CheckboxFieldUncontrolledProps extends React.ComponentProps<'input'> { + label: string +} + +export const CheckboxFieldUncontrolled = ({ + label, + ...rest +}: CheckboxFieldUncontrolledProps) => ( +
+ + +
+) diff --git a/src/components/Internal/Interactions.tsx b/src/components/Internal/Interactions.tsx new file mode 100644 index 0000000..534c910 --- /dev/null +++ b/src/components/Internal/Interactions.tsx @@ -0,0 +1,42 @@ +import { Addressable } from 'types' +import { abbreviateNumber } from 'utils' +import { Zap } from './Zap' +import { Reactions } from './Reactions' + +type InteractionsProps = { + addressable: Addressable + commentCount: number +} + +export const Interactions = ({ + addressable, + commentCount +}: InteractionsProps) => { + return ( +
+
+ +
+
+ + + +
+

+ {abbreviateNumber(commentCount)} +

+
+
+ + +
+
+ ) +} diff --git a/src/components/Internal/PublishDetails.tsx b/src/components/Internal/PublishDetails.tsx new file mode 100644 index 0000000..88cef78 --- /dev/null +++ b/src/components/Internal/PublishDetails.tsx @@ -0,0 +1,86 @@ +import { formatDate } from 'date-fns' + +type PublishDetailsProps = { + published_at: number + edited_at: number + site: string +} + +export const PublishDetails = ({ + published_at, + edited_at, + site +}: PublishDetailsProps) => { + return ( +
+
+
+ + + +

+ {formatDate( + (published_at !== -1 ? published_at : edited_at) * 1000, + 'dd/MM/yyyy hh:mm:ss aa' + )} +

+
+
+ + + +

+ {formatDate(edited_at * 1000, 'dd/MM/yyyy hh:mm:ss aa')} +

+
+ + + + +

{site}

+
+
+
+ ) +} diff --git a/src/pages/mod/internal/reactions/index.tsx b/src/components/Internal/Reactions.tsx similarity index 79% rename from src/pages/mod/internal/reactions/index.tsx rename to src/components/Internal/Reactions.tsx index d50a787..d990429 100644 --- a/src/pages/mod/internal/reactions/index.tsx +++ b/src/components/Internal/Reactions.tsx @@ -1,11 +1,12 @@ +import { Dots } from 'components/Spinner' import { useReactions } from 'hooks' -import { ModDetails } from 'types' +import { Addressable } from 'types' type ReactionsProps = { - modDetails: ModDetails + addressable: Addressable } -export const Reactions = ({ modDetails }: ReactionsProps) => { +export const Reactions = ({ addressable }: ReactionsProps) => { const { isDataLoaded, likesCount, @@ -14,20 +15,18 @@ export const Reactions = ({ modDetails }: ReactionsProps) => { hasReactedPositively, hasReactedNegatively } = useReactions({ - pubkey: modDetails.author, - eTag: modDetails.id, - aTag: modDetails.aTag + pubkey: addressable.author, + eTag: addressable.id, + aTag: addressable.aTag }) - if (!isDataLoaded) return null - return ( <>
handleReaction(true)} + onClick={isDataLoaded ? () => handleReaction(true) : undefined} >
{
-

{likesCount}

+

+ {isDataLoaded ? likesCount : } +

@@ -50,7 +51,7 @@ export const Reactions = ({ modDetails }: ReactionsProps) => { className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown ${ hasReactedNegatively ? 'IBMSMSMBSS_D_CRDActive' : '' }`} - onClick={() => handleReaction()} + onClick={isDataLoaded ? () => handleReaction() : undefined} >
{
-

{disLikesCount}

+

+ {isDataLoaded ? disLikesCount : } +

diff --git a/src/pages/mod/internal/zap/index.tsx b/src/components/Internal/Zap.tsx similarity index 88% rename from src/pages/mod/internal/zap/index.tsx rename to src/components/Internal/Zap.tsx index 996c8d2..0c5cb7a 100644 --- a/src/pages/mod/internal/zap/index.tsx +++ b/src/components/Internal/Zap.tsx @@ -7,14 +7,14 @@ import { } from 'hooks' import { useState } from 'react' import { toast } from 'react-toastify' -import { ModDetails } from 'types' +import { Addressable } from 'types' import { abbreviateNumber } from 'utils' type ZapProps = { - modDetails: ModDetails + addressable: Addressable } -export const Zap = ({ modDetails }: ZapProps) => { +export const Zap = ({ addressable }: ZapProps) => { const [isOpen, setIsOpen] = useState(false) const [totalZappedAmount, setTotalZappedAmount] = useState(0) const [hasZapped, setHasZapped] = useState(false) @@ -26,9 +26,9 @@ export const Zap = ({ modDetails }: ZapProps) => { useDidMount(() => { getTotalZapAmount( - modDetails.author, - modDetails.id, - modDetails.aTag, + addressable.author, + addressable.id, + addressable.aTag, userState.user?.pubkey as string ) .then((res) => { @@ -70,9 +70,9 @@ export const Zap = ({ modDetails }: ZapProps) => {
{isOpen && ( setIsOpen(false)} diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx index 6abd397..c54b300 100644 --- a/src/components/ModCard.tsx +++ b/src/components/ModCard.tsx @@ -12,7 +12,7 @@ import { useComments } from 'hooks/useComments' export const ModCard = React.memo((props: ModDetails) => { const [totalZappedAmount, setTotalZappedAmount] = useState(0) const [commentCount, setCommentCount] = useState(0) - const { commentEvents } = useComments(props) + const { commentEvents } = useComments(props.author, props.aTag) const { likesCount, disLikesCount } = useReactions({ pubkey: props.author, eTag: props.id, diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index c3f5a02..7411c47 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -192,7 +192,7 @@ export const ModForm = ({ existingModData }: ModFormProps) => { return } - const uuid = uuidv4() + const uuid = formState.dTag || uuidv4() const currentTimeStamp = now() const aTag = @@ -204,7 +204,7 @@ export const ModForm = ({ existingModData }: ModFormProps) => { pubkey: hexPubkey, content: formState.body, tags: [ - ['d', formState.dTag || uuid], + ['d', uuid], ['a', aTag], ['r', formState.rTag], ['t', T_TAG_VALUE], diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 0000000..786c6ba --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,9 @@ +import styles from '../styles/dotsSpinner.module.scss' + +export const Spinner = () => ( +
+
+
+) + +export const Dots = () => diff --git a/src/pages/mod/internal/comment/index.tsx b/src/components/comment/index.tsx similarity index 88% rename from src/pages/mod/internal/comment/index.tsx rename to src/components/comment/index.tsx index f238524..b9a87b3 100644 --- a/src/pages/mod/internal/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -1,4 +1,5 @@ import { NDKEvent } from '@nostr-dev-kit/ndk' +import { Dots, Spinner } from 'components/Spinner' import { ZapPopUp } from 'components/Zap' import { formatDate } from 'date-fns' import { @@ -21,9 +22,9 @@ import { Link } from 'react-router-dom' import { toast } from 'react-toastify' import { getProfilePageRoute } from 'routes' import { + Addressable, CommentEvent, CommentEventStatus, - ModDetails, UserProfile } from 'types/index.ts' import { abbreviateNumber, hexToNpub, log, LogType, now } from 'utils' @@ -44,18 +45,30 @@ type FilterOptions = { } type Props = { - modDetails: ModDetails + addressable: Addressable setCommentCount: Dispatch> } -export const Comments = ({ modDetails, setCommentCount }: Props) => { +export const Comments = ({ addressable, setCommentCount }: Props) => { const { ndk, publish } = useNDKContext() - const { commentEvents, setCommentEvents } = useComments(modDetails) + const { commentEvents, setCommentEvents } = useComments( + addressable.author, + addressable.aTag + ) const [filterOptions, setFilterOptions] = useState({ sort: SortByEnum.Latest, author: AuthorFilterEnum.All_Comments }) + const [isLoading, setIsLoading] = useState(true) + useEffect(() => { + // Initial loading to indicate comments fetching (stop after 5 seconds) + const t = window.setTimeout(() => setIsLoading(false), 5000) + return () => { + window.clearTimeout(t) + } + }, []) + useEffect(() => { setCommentCount(commentEvents.length) }, [commentEvents, setCommentCount]) @@ -84,9 +97,9 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => { kind: kinds.ShortTextNote, created_at: now(), tags: [ - ['e', modDetails.id], - ['a', modDetails.aTag], - ['p', modDetails.author] + ['e', addressable.id], + ['a', addressable.aTag], + ['p', addressable.author] ] } @@ -172,11 +185,21 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => { return true } + const handleDiscoveredClick = () => { + setVisible(commentEvents) + } + const [visible, setVisible] = useState([]) + useEffect(() => { + if (isLoading) { + setVisible(commentEvents) + } + }, [commentEvents, isLoading]) + const comments = useMemo(() => { - let filteredComments = commentEvents + let filteredComments = visible if (filterOptions.author === AuthorFilterEnum.Creator_Comments) { filteredComments = filteredComments.filter( - (comment) => comment.pubkey === modDetails.author + (comment) => comment.pubkey === addressable.author ) } @@ -187,13 +210,28 @@ export const Comments = ({ modDetails, setCommentCount }: Props) => { } return filteredComments - }, [commentEvents, filterOptions, modDetails.author]) + }, [visible, filterOptions.author, filterOptions.sort, addressable.author]) + const discoveredCount = commentEvents.length - visible.length return (

Comments

- + {/* Hide comment form if aTag is missing */} + {!!addressable.aTag && } +
+ {isLoading ? ( + + ) : ( + + )} +
{
@@ -443,15 +481,13 @@ const Reactions = (props: Event) => { eTag: props.id }) - if (!isDataLoaded) return null - return ( <>
handleReaction(true)} + onClick={isDataLoaded ? () => handleReaction(true) : undefined} > { > -

{likesCount}

+

+ {isDataLoaded ? likesCount : } +

@@ -472,7 +510,7 @@ const Reactions = (props: Event) => { className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${ hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : '' }`} - onClick={() => handleReaction()} + onClick={isDataLoaded ? () => handleReaction() : undefined} > { > -

{disLikesCount}

+

+ {isDataLoaded ? disLikesCount : } +

diff --git a/src/constants.ts b/src/constants.ts index e2838b0..99926cf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -20,6 +20,12 @@ export const LANDING_PAGE_DATA = { 'Cyberpunk 2077', 'ELDEN RING', 'The Coffin of Andy and Leyley' + ], + featuredBlogPosts: [ + 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2hs4n6g93x55t6wd8hwejlv9k8qcj8tuu5y7fh3ju', + 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2j6vzlfsekk3nstgu9s5m4wpexz6j30pzxzzljm5k', + 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2kwjtwvahns3n0tf8j6kjxggkkz4mff499ge7xzsz', + 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2573jhg9trsu6vgav9gnn4dffkzk2ww3yrjejnc2s' ] } // we use this object to check if a user has reacted positively or negatively to a post @@ -112,7 +118,8 @@ export const REACTIONS = { export const MAX_MODS_PER_PAGE = 10 export const MAX_GAMES_PER_PAGE = 10 - // todo: add game and mod fallback image here export const FALLBACK_PROFILE_IMAGE = 'https://image.nostr.build/a305f4b43f74af3c6dcda42e6a798105a56ac1e3e7b74d7bef171896b3ba7520.png' + +export const PROFILE_BLOG_FILTER_LIMIT = 20 diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index b76f3f2..ed80c1f 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -21,7 +21,8 @@ import { log, LogType, npubToHex, - orderEventsChronologically + orderEventsChronologically, + timeout } from 'utils' type FetchModsOptions = { @@ -32,7 +33,7 @@ type FetchModsOptions = { author?: string } -interface NDKContextType { +export interface NDKContextType { ndk: NDK fetchMods: (opts: FetchModsOptions) => Promise fetchEvents: (filter: NDKFilter) => Promise @@ -241,8 +242,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { hexKey: string, userRelaysType: UserRelaysType ): Promise => { - // Find the user's relays. - const relayUrls = await getRelayListForUser(hexKey, ndk) + // Find the user's relays (10s timeout). + const relayUrls = await Promise.race([ + getRelayListForUser(hexKey, ndk), + timeout(10000) + ]) .then((ndkRelayList) => { if (ndkRelayList) return ndkRelayList[userRelaysType] return [] // Return an empty array if ndkRelayList is undefined diff --git a/src/hooks/useComments.ts b/src/hooks/useComments.ts index fb57eb8..8b107bc 100644 --- a/src/hooks/useComments.ts +++ b/src/hooks/useComments.ts @@ -7,22 +7,30 @@ import { NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk' import { useEffect, useState } from 'react' -import { CommentEvent, ModDetails, UserRelaysType } from 'types' +import { CommentEvent, UserRelaysType } from 'types' import { log, LogType, timeout } from 'utils' import { useNDKContext } from './useNDKContext' -export const useComments = (mod: ModDetails) => { +export const useComments = ( + author: string | undefined, + aTag: string | undefined +) => { const { ndk } = useNDKContext() const [commentEvents, setCommentEvents] = useState([]) useEffect(() => { + if (!(author && aTag)) { + // Author and aTag are required + return + } + let subscription: NDKSubscription // Define the subscription variable here for cleanup const setupSubscription = async () => { // Find the mod author's relays. const authorReadRelays = await Promise.race([ - getRelayListForUser(mod.author, ndk), + getRelayListForUser(author, ndk), timeout(10 * 1000) // add a 10 sec timeout ]) .then((ndkRelayList) => { @@ -33,7 +41,7 @@ export const useComments = (mod: ModDetails) => { log( true, LogType.Error, - `An error occurred in fetching user's (${mod.author}) ${UserRelaysType.Read}`, + `An error occurred in fetching user's (${author}) ${UserRelaysType.Read}`, err ) return [] as string[] @@ -41,7 +49,7 @@ export const useComments = (mod: ModDetails) => { const filter: NDKFilter = { kinds: [NDKKind.Text], - '#a': [mod.aTag] + '#a': [aTag] } const relayUrls = new Set() @@ -92,7 +100,7 @@ export const useComments = (mod: ModDetails) => { subscription.stop() } } - }, [mod.aTag, mod.author, ndk]) + }, [aTag, author, ndk]) return { commentEvents, diff --git a/src/hooks/useNDKContext.ts b/src/hooks/useNDKContext.ts index f7383df..20acf27 100644 --- a/src/hooks/useNDKContext.ts +++ b/src/hooks/useNDKContext.ts @@ -1,4 +1,4 @@ -import { NDKContext } from 'contexts/NDKContext' +import { NDKContext, NDKContextType } from 'contexts/NDKContext' import { useContext } from 'react' export const useNDKContext = () => { @@ -9,5 +9,5 @@ export const useNDKContext = () => { 'NDKContext should not be used in out component tree hierarchy' ) - return { ...ndkContext } + return { ...ndkContext } as NDKContextType } diff --git a/src/hooks/useReactions.ts b/src/hooks/useReactions.ts index 574c3eb..0f029bb 100644 --- a/src/hooks/useReactions.ts +++ b/src/hooks/useReactions.ts @@ -5,7 +5,7 @@ import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { useMemo, useState } from 'react' import { toast } from 'react-toastify' import { UserRelaysType } from 'types' -import { abbreviateNumber, log, LogType, now } from 'utils' +import { abbreviateNumber, log, LogType, now, timeout } from 'utils' type UseReactionsParams = { pubkey: string @@ -32,7 +32,11 @@ export const useReactions = (params: UseReactionsParams) => { filter['#e'] = [params.eTag] } - fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read) + // 1 minute timeout + Promise.race([ + fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read), + timeout(60000) + ]) .then((events) => { setReactionEvents(events) }) diff --git a/src/layout/header.tsx b/src/layout/header.tsx index 011b7cf..008915d 100644 --- a/src/layout/header.tsx +++ b/src/layout/header.tsx @@ -212,7 +212,7 @@ export const Header = () => { About Blog @@ -379,16 +379,14 @@ const RegisterButtonWithDialog = () => { Browser Extensions (Windows)

- Once you create your "account" on any of these ( - - Here's a quick video guide - - ), come back and click login, then sign-in with - extension. -

+ target='blank_'>guide post to help with this process.

+
+ +
{
-
-
-

- Creator's Blog Posts (WIP) -

-
- - - -
-
-
+
@@ -972,126 +964,6 @@ const Body = ({ ) } -type InteractionsProps = { - modDetails: ModDetails - commentCount: number -} - -const Interactions = ({ modDetails, commentCount }: InteractionsProps) => { - return ( - - ) -} - -type PublishDetailsProps = { - published_at: number - edited_at: number - site: string -} - -const PublishDetails = ({ - published_at, - edited_at, - site -}: PublishDetailsProps) => { - return ( -
-
-
- - - -

- {formatDate( - (published_at !== -1 ? published_at : edited_at) * 1000, - 'dd/MM/yyyy hh:mm:ss aa' - )} -

-
-
- - - -

- {formatDate(edited_at * 1000, 'dd/MM/yyyy hh:mm:ss aa')} -

-
- - - - -

{site}

-
-
-
- ) -} - const Download = ({ url, hash, @@ -1349,3 +1221,49 @@ const Download = ({
) } + +const DisplayModAuthorBlogs = () => { + const { naddr } = useParams() + const [blogs, setBlogs] = useState[]>() + const { fetchEvents } = useNDKContext() + + useDidMount(() => { + const fetchBlogs = async () => { + try { + const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) + const { pubkey } = decoded.data + const latestBlogPosts = await fetchEvents({ + authors: [pubkey], + kinds: [kinds.LongFormArticle], + limit: 3 + }) + setBlogs(latestBlogPosts.map(extractBlogCardDetails)) + } catch (error) { + log( + true, + LogType.Error, + 'An error occurred in fetching blog details from relays', + error + ) + return null + } + } + + fetchBlogs() + }) + + if (!blogs?.length) return null + + return ( +
+
+

Creator's Blog Posts

+
+ {blogs?.map((b) => ( + + ))} +
+
+
+ ) +} diff --git a/src/pages/profile.tsx b/src/pages/profile/index.tsx similarity index 77% rename from src/pages/profile.tsx rename to src/pages/profile/index.tsx index 84e2293..56758a6 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile/index.tsx @@ -5,7 +5,7 @@ import { ModFilter } from 'components/ModsFilter' import { Pagination } from 'components/Pagination' import { ProfileSection } from 'components/ProfileSection' import { Tabs } from 'components/Tabs' -import { MOD_FILTER_LIMIT } from '../constants' +import { MOD_FILTER_LIMIT, PROFILE_BLOG_FILTER_LIMIT } from '../../constants' import { useAppSelector, useFilteredMods, @@ -14,17 +14,24 @@ import { useNDKContext, useNSFWList } from 'hooks' -import { nip19, UnsignedEvent } from 'nostr-tools' -import { useCallback, useEffect, useRef, useState } from 'react' -import { useParams, Navigate, Link } from 'react-router-dom' +import { kinds, UnsignedEvent } from 'nostr-tools' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Link, useLoaderData, useNavigation } from 'react-router-dom' import { toast } from 'react-toastify' -import { appRoutes, getProfilePageRoute } from 'routes' -import { FilterOptions, ModDetails, UserRelaysType } from 'types' +import { appRoutes } from 'routes' +import { + BlogCardDetails, + FilterOptions, + ModDetails, + ModeratedFilter, + NSFWFilter, + SortBy, + UserRelaysType +} from 'types' import { copyTextToClipboard, DEFAULT_FILTER_OPTIONS, - log, - LogType, + extractBlogCardDetails, now, npubToHex, scrollIntoView, @@ -32,65 +39,27 @@ import { signAndPublish } from 'utils' import { CheckboxField } from 'components/Inputs' -import { useProfile } from 'hooks/useProfile' +import { ProfilePageLoaderResult } from './loader' +import { BlogCard } from 'components/BlogCard' export const ProfilePage = () => { - // Try to decode nprofile parameter - const { nprofile } = useParams() - let profilePubkey: string | undefined - try { - const value = nprofile - ? nip19.decode(nprofile as `nprofile1${string}`) - : undefined - profilePubkey = value?.data.pubkey - } catch (error) { - // Silently ignore and redirect to home or logged in user - log(true, LogType.Error, 'Failed to decode nprofile.', error) - } - + const { + profilePubkey, + profile, + isBlocked: _isBlocked, + isOwnProfile + } = useLoaderData() as ProfilePageLoaderResult const scrollTargetRef = useRef(null) const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext() const userState = useAppSelector((state) => state.user) - const isOwnProfile = - userState.auth && userState.user?.pubkey === profilePubkey const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - const profile = useProfile(profilePubkey) - const displayName = profile?.displayName || profile?.name || '[name not set up]' const [showReportPopUp, setShowReportPopUp] = useState(false) - const [isBlocked, setIsBlocked] = useState(false) - useEffect(() => { - if (userState.auth && userState.user?.pubkey) { - const userHexKey = userState.user.pubkey as string - - const muteListFilter: NDKFilter = { - kinds: [NDKKind.MuteList], - authors: [userHexKey] - } - - fetchEventFromUserRelays( - muteListFilter, - userHexKey, - UserRelaysType.Write - ).then((event) => { - if (event) { - // get a list of tags - const tags = event.tags - const blocked = - tags.findIndex( - (item) => item[0] === 'p' && item[1] === profilePubkey - ) !== -1 - - setIsBlocked(blocked) - } - }) - } - }, [userState, profilePubkey, fetchEventFromUserRelays]) - + const [isBlocked, setIsBlocked] = useState(_isBlocked) const handleBlock = async () => { if (!profilePubkey) { toast.error(`Something went wrong. Unable to find reported user's pubkey`) @@ -279,6 +248,7 @@ export const ProfilePage = () => { setIsLoading(true) switch (tab) { case 0: + setLoadingSpinnerDesc('Fetching mods..') fetchMods({ source: filterOptions.source, author: profilePubkey }) .then((res) => { setMods(res) @@ -302,22 +272,6 @@ export const ProfilePage = () => { profilePubkey ) - // Redirect route - let profileRoute = appRoutes.home - if (!nprofile && userState.auth && userState.user) { - // Redirect to user's profile is no profile is linked - const userHexKey = npubToHex(userState.user.npub as string) - - if (userHexKey) { - profileRoute = getProfilePageRoute( - nip19.nprofileEncode({ - pubkey: userHexKey - }) - ) - } - } - if (!profilePubkey) return - return (
@@ -482,7 +436,7 @@ export const ProfilePage = () => { )} - {tab === 1 && <>WIP} + {tab === 1 && } {tab === 2 && <>WIP}
@@ -703,3 +657,189 @@ const ReportUserPopup = ({ ) } + +const ProfileTabBlogs = () => { + const { profile, muteLists, nsfwList } = + useLoaderData() as ProfilePageLoaderResult + const navigation = useNavigation() + const { fetchEvents } = useNDKContext() + const [filterOptions] = useLocalStorage('filter-blog', DEFAULT_FILTER_OPTIONS) + const [isLoading, setIsLoading] = useState(true) + const blogfilter: NDKFilter = useMemo(() => { + const filter: NDKFilter = { + authors: [profile?.pubkey as string], + kinds: [kinds.LongFormArticle] + } + + const host = window.location.host + if (filterOptions.source === host) { + filter['#r'] = [host] + } + + if (filterOptions.nsfw === NSFWFilter.Only_NSFW) { + filter['#L'] = ['content-warning'] + } + + return filter + }, [filterOptions.nsfw, filterOptions.source, profile?.pubkey]) + + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(false) + const [blogs, setBlogs] = useState[]>([]) + useEffect(() => { + if (profile) { + // Initial blog fetch, go beyond limit to check for next + const filter: NDKFilter = { + ...blogfilter, + limit: PROFILE_BLOG_FILTER_LIMIT + 1 + } + fetchEvents(filter) + .then((events) => { + setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr)) + setHasMore(events.length > PROFILE_BLOG_FILTER_LIMIT) + }) + .finally(() => { + setIsLoading(false) + }) + } + }, [blogfilter, fetchEvents, profile]) + + const handleNext = useCallback(() => { + if (isLoading) return + + const last = blogs.length > 0 ? blogs[blogs.length - 1] : undefined + if (last?.published_at) { + const until = last?.published_at - 1 + const nextFilter = { + ...blogfilter, + limit: PROFILE_BLOG_FILTER_LIMIT + 1, + until + } + setIsLoading(true) + fetchEvents(nextFilter) + .then((events) => { + const nextBlogs = events.map(extractBlogCardDetails) + setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT) + setPage((prev) => prev + 1) + setBlogs( + nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((b) => b.naddr) + ) + }) + .finally(() => setIsLoading(false)) + } + }, [blogfilter, blogs, fetchEvents, isLoading]) + + const handlePrev = useCallback(() => { + if (isLoading) return + + const first = blogs.length > 0 ? blogs[0] : undefined + if (first?.published_at) { + const since = first.published_at + 1 + const prevFilter = { + ...blogfilter, + limit: PROFILE_BLOG_FILTER_LIMIT, + since + } + setIsLoading(true) + fetchEvents(prevFilter) + .then((events) => { + setHasMore(true) + setPage((prev) => prev - 1) + setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr)) + }) + .finally(() => setIsLoading(false)) + } + }, [blogfilter, blogs, fetchEvents, isLoading]) + + const userState = useAppSelector((state) => state.user) + const moderatedAndSortedBlogs = useMemo(() => { + let _blogs = blogs || [] + const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB + const isOwner = + userState.user?.pubkey && userState.user.pubkey === profile?.pubkey + const isUnmoderatedFully = + filterOptions.moderated === ModeratedFilter.Unmoderated_Fully + + // Add nsfw tag to blogs included in nsfwList + if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) { + _blogs = _blogs.map((b) => { + return !b.nsfw && b.aTag && nsfwList.includes(b.aTag) + ? { ...b, nsfw: true } + : b + }) + } + + // Filter nsfw (Hide_NSFW option) + _blogs = _blogs.filter( + (b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW) + ) + + // Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully" + // Allow "Unmoderated Fully" when author visits own profile + if (!((isAdmin || isOwner) && isUnmoderatedFully)) { + _blogs = _blogs.filter( + (b) => + !muteLists.admin.authors.includes(b.author!) && + !muteLists.admin.replaceableEvents.includes(b.aTag!) + ) + } + + if (filterOptions.moderated === ModeratedFilter.Moderated) { + _blogs = _blogs.filter( + (b) => + !muteLists.user.authors.includes(b.author!) && + !muteLists.user.replaceableEvents.includes(b.aTag!) + ) + } + + if (filterOptions.sort === SortBy.Latest) { + _blogs.sort((a, b) => + a.published_at && b.published_at ? b.published_at - a.published_at : 0 + ) + } else if (filterOptions.sort === SortBy.Oldest) { + _blogs.sort((a, b) => + a.published_at && b.published_at ? a.published_at - b.published_at : 0 + ) + } + + return _blogs + }, [ + blogs, + filterOptions.moderated, + filterOptions.nsfw, + filterOptions.sort, + muteLists.admin.authors, + muteLists.admin.replaceableEvents, + muteLists.user.authors, + muteLists.user.replaceableEvents, + nsfwList, + profile?.pubkey, + userState.user?.npub, + userState.user?.pubkey + ]) + + return ( + <> + {(isLoading || navigation.state !== 'idle') && ( + + )} + + + +
+ {moderatedAndSortedBlogs.map((b) => ( + + ))} +
+ + {!(page === 1 && !hasMore) && ( + + )} + + ) +} diff --git a/src/pages/profile/loader.ts b/src/pages/profile/loader.ts new file mode 100644 index 0000000..f3ac4c0 --- /dev/null +++ b/src/pages/profile/loader.ts @@ -0,0 +1,147 @@ +import { NDKContextType } from 'contexts/NDKContext' +import { nip19 } from 'nostr-tools' +import { LoaderFunctionArgs, redirect } from 'react-router-dom' +import { appRoutes, getProfilePageRoute } from 'routes' +import { store } from 'store' +import { MuteLists, UserProfile } from 'types' +import { log, LogType, npubToHex } from 'utils' + +export interface ProfilePageLoaderResult { + profilePubkey: string + profile: UserProfile + isBlocked: boolean + isOwnProfile: boolean + muteLists: { + admin: MuteLists + user: MuteLists + } + nsfwList: string[] +} + +export const profileRouteLoader = + (ndkContext: NDKContextType) => + async ({ params }: LoaderFunctionArgs) => { + // Try to decode nprofile parameter + const { nprofile } = params + let profilePubkey: string | undefined + try { + // Decode if it starts with nprofile1 + if (nprofile?.startsWith('nprofile1')) { + const value = nprofile + ? nip19.decode(nprofile as `nprofile1${string}`) + : undefined + profilePubkey = value?.data.pubkey + } else if (nprofile?.startsWith('npub1')) { + // Try to get hex from the npub and encode it to nprofile + const value = npubToHex(nprofile) + if (value) { + return redirect( + getProfilePageRoute( + nip19.nprofileEncode({ + pubkey: value + }) + ) + ) + } + } + } catch (error) { + // Silently ignore and redirect to home or logged in user + log(true, LogType.Error, 'Failed to decode nprofile.', error) + } + + // Get the current state + const userState = store.getState().user + + // Check if current user is logged in + let userPubkey: string | undefined + if (userState.auth && userState.user?.pubkey) { + userPubkey = userState.user.pubkey as string + } + + // Redirect if profile naddr is missing + // - home if user is not logged + let profileRoute = appRoutes.home + if (!profilePubkey && userPubkey) { + // - own profile + profileRoute = getProfilePageRoute( + nip19.nprofileEncode({ + pubkey: userPubkey + }) + ) + } + if (!profilePubkey) return redirect(profileRoute) + + // Empty result + const result: ProfilePageLoaderResult = { + profilePubkey: profilePubkey, + profile: {}, + isBlocked: false, + isOwnProfile: false, + muteLists: { + admin: { + authors: [], + replaceableEvents: [] + }, + user: { + authors: [], + replaceableEvents: [] + } + }, + nsfwList: [] + } + + // Check if user the user is logged in + if (userState.auth && userState.user?.pubkey) { + result.isOwnProfile = userState.user.pubkey === profilePubkey + } + + const settled = await Promise.allSettled([ + ndkContext.findMetadata(profilePubkey), + ndkContext.getMuteLists(userPubkey), + ndkContext.getNSFWList() + ]) + + // Check the profile event result + const profileEventResult = settled[0] + if (profileEventResult.status === 'fulfilled' && profileEventResult.value) { + result.profile = profileEventResult.value + } else if (profileEventResult.status === 'rejected') { + log( + true, + LogType.Error, + 'Failed to fetch profile.', + profileEventResult.reason + ) + } + + // Check the profile event result + const muteListResult = settled[1] + if (muteListResult.status === 'fulfilled' && muteListResult.value) { + result.muteLists = muteListResult.value + + // Check if user has blocked this profile + result.isBlocked = result.muteLists.user.authors.includes(profilePubkey) + } else if (muteListResult.status === 'rejected') { + log( + true, + LogType.Error, + 'Failed to fetch mutelist.', + muteListResult.reason + ) + } + + // Check the profile event result + const nsfwListResult = settled[2] + if (nsfwListResult.status === 'fulfilled' && nsfwListResult.value) { + result.nsfwList = nsfwListResult.value + } else if (nsfwListResult.status === 'rejected') { + log( + true, + LogType.Error, + 'Failed to fetch mutelist.', + nsfwListResult.reason + ) + } + + return result + } diff --git a/src/pages/write.tsx b/src/pages/write.tsx deleted file mode 100644 index 58813a5..0000000 --- a/src/pages/write.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { CheckboxField, InputField } from '../components/Inputs' -import { ProfileSection } from '../components/ProfileSection' -import { useAppSelector } from '../hooks' -import '../styles/innerPage.css' -import '../styles/styles.css' -import '../styles/write.css' - -export const WritePage = () => { - const userState = useAppSelector((state) => state.user) - - return ( -
-
-
-
-
-
-

- Write a blog post (WIP) -

-
-
- {}} - /> - {}} - /> - {}} - /> - {}} - /> - {}} - type='stylized' - /> -
- -
-
-
- {userState.auth && userState.user?.pubkey && ( - - )} -
-
-
-
- ) -} diff --git a/src/pages/write/action.tsx b/src/pages/write/action.tsx new file mode 100644 index 0000000..61bb7ad --- /dev/null +++ b/src/pages/write/action.tsx @@ -0,0 +1,189 @@ +import { NDKContextType } from 'contexts/NDKContext' +import { ActionFunctionArgs, redirect } from 'react-router-dom' +import { getBlogPageRoute } from 'routes' +import { BlogFormErrors, BlogEventSubmitForm, BlogEventEditForm } from 'types' +import { + isReachable, + isValidImageUrl, + log, + LogType, + now, + parseFormData +} from 'utils' +import TurndownService from 'turndown' +import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools' +import { toast } from 'react-toastify' +import { NDKEvent } from '@nostr-dev-kit/ndk' +import { v4 as uuidv4 } from 'uuid' +import { store } from 'store' + +export const writeRouteAction = + (ndkContext: NDKContextType) => + async ({ params, request }: ActionFunctionArgs) => { + // Get the current state + const userState = store.getState().user + let hexPubkey: string + if (userState.auth && userState.user?.pubkey) { + hexPubkey = userState.user.pubkey as string + } else { + try { + hexPubkey = (await window.nostr?.getPublicKey()) as string + } catch (error) { + if (error instanceof Error) { + log(true, LogType.Error, 'Failed to get public key.', error) + } + + toast.error('Failed to get public key.') + return null + } + } + + if (!hexPubkey) { + toast.error('Could not get pubkey') + return null + } + + // Get the form data from submit request + const formData = await request.formData() + + // Parse the the data + const formSubmit = parseFormData( + formData + ) + + // Check for errors + const formErrors = await validateFormData(formSubmit) + + // Return earily if there are any errors + if (Object.keys(formErrors).length) return formErrors + + // Get the markdown from the html + const turndownService = new TurndownService() + const content = turndownService.turndown(formSubmit.content!) + + // Check if we are editing or this is a new blog + const { naddr } = params + const isEditing = + naddr && request.method === 'PUT' && isEditForm(formSubmit) + const formEdit = isEditing ? formSubmit : undefined + + const currentTimeStamp = now() + + // Get the existing edited fields or new ones + const uuid = isEditing && formEdit?.dTag ? formSubmit.dTag : uuidv4() + const rTag = + isEditing && formEdit?.rTag ? formEdit.rTag : window.location.host + const published_at = + isEditing && formEdit?.published_at + ? formEdit.published_at + : currentTimeStamp + + const aTag = `${kinds.LongFormArticle}:${hexPubkey}:${uuid}` + const tTags = formSubmit + .tags!.toLowerCase() + .split(',') + .map((t) => ['t', t]) + + const tags = [ + ['d', uuid], + ['a', aTag], + ['r', rTag], + ['published_at', published_at.toString()], + ['title', formSubmit.title!], + ['image', formSubmit.image!], + ['summary', formSubmit.summary!], + ...tTags + ] + + // Add NSFW tag, L label namespace standardized tag + // https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags + if (formSubmit.nsfw === 'on') tags.push(['L', 'content-warning']) + + const unsignedEvent: UnsignedEvent = { + kind: kinds.LongFormArticle, + created_at: currentTimeStamp, + pubkey: hexPubkey, + content: content, + tags: tags + } + + try { + const signedEvent = await window.nostr + ?.signEvent(unsignedEvent) + .then((event) => event as Event) + + if (!signedEvent) { + toast.error('Failed to sign the event!') + return null + } + + const ndkEvent = new NDKEvent(ndkContext.ndk, signedEvent) + const publishedOnRelays = await ndkContext.publish(ndkEvent) + + // Handle cases where publishing failed or succeeded + if (publishedOnRelays.length === 0) { + toast.error('Failed to publish event on any relay.') + return null + } else { + toast.success( + `Event published successfully to the following relays\n\n${publishedOnRelays.join( + '\n' + )}` + ) + const naddr = nip19.naddrEncode({ + identifier: uuid, + pubkey: signedEvent.pubkey, + kind: signedEvent.kind, + relays: publishedOnRelays + }) + return redirect(getBlogPageRoute(naddr)) + } + } catch (error) { + log(true, LogType.Error, 'Failed to sign the event!', error) + toast.error('Failed to sign the event!') + return null + } + } + +const validateFormData = async ( + formData: Partial +): Promise => { + const errors: BlogFormErrors = {} + + if (!formData.title || formData.title.trim() === '') { + errors.title = 'Title field can not be empty' + } + + if ( + !formData.content || + formData.content.trim() === '' || + formData.content.trim() === '

' + ) { + errors.content = 'Content field can not be empty' + } + + if (!formData.summary || formData.summary.trim() === '') { + errors.summary = 'Summary field can not be empty' + } + + if (!formData.tags || formData.tags.trim() === '') { + errors.tags = 'Tags field can not be empty' + } + + if (!formData.image || formData.image.trim() === '') { + errors.image = 'Image url field can not be empty' + } else if ( + !isValidImageUrl(formData.image) || + !(await isReachable(formData.image)) + ) { + errors.image = 'Image must be a valid and reachable' + } + + return errors +} + +function isEditForm( + form: Partial +): form is BlogEventEditForm { + return (form as BlogEventEditForm).dTag !== undefined +} diff --git a/src/pages/write/index.tsx b/src/pages/write/index.tsx new file mode 100644 index 0000000..870bb2b --- /dev/null +++ b/src/pages/write/index.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react' +import { + Form, + useActionData, + useLoaderData, + useNavigation +} from 'react-router-dom' +import { + CheckboxFieldUncontrolled, + InputError, + InputFieldUncontrolled, + MenuBar +} from '../../components/Inputs' +import { ProfileSection } from '../../components/ProfileSection' +import { useAppSelector } from '../../hooks' +import { BlogFormErrors, BlogPageLoaderResult } from 'types' +import '../../styles/innerPage.css' +import '../../styles/styles.css' +import '../../styles/write.css' +import { LoadingSpinner } from 'components/LoadingSpinner' +import { marked } from 'marked' +import DOMPurify from 'dompurify' +import { EditorContent, useEditor } from '@tiptap/react' +import StarterKit from '@tiptap/starter-kit' +import Link from '@tiptap/extension-link' +import Image from '@tiptap/extension-image' + +export const WritePage = () => { + const userState = useAppSelector((state) => state.user) + const data = useLoaderData() as BlogPageLoaderResult + const formErrors = useActionData() as BlogFormErrors + const navigation = useNavigation() + + const blog = data?.blog + const title = data?.blog ? 'Edit blog post' : 'Submit a blog post' + const html = marked.parse(blog?.content || '', { async: false }) + const sanitized = DOMPurify.sanitize(html) + const [content, setContent] = useState(sanitized) + const editor = useEditor({ + content: content, + extensions: [ + StarterKit, + Link, + Image.configure({ + inline: true, + HTMLAttributes: { + class: 'IBMSMSMBSSPostImg' + } + }) + ], + onUpdate: ({ editor }) => { + setContent(editor.getHTML()) + } + }) + + return ( +
+
+
+
+
+
+

{title}

+
+ {navigation.state === 'loading' && ( + + )} + {navigation.state === 'submitting' && ( + + )} +
+ + {editor && ( +
+ +
+ + +
+ {typeof formErrors?.content !== 'undefined' && ( + + )} + +
+ )} + + + + + {typeof blog?.dTag !== 'undefined' && ( + + )} + {typeof blog?.rTag !== 'undefined' && ( + + )} + {typeof blog?.published_at !== 'undefined' && ( + + )} +
+ +
+ +
+ {userState.auth && userState.user?.pubkey && ( + + )} +
+
+
+
+ ) +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 18e4d63..aec86c6 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,21 +1,29 @@ import { createBrowserRouter } from 'react-router-dom' +import { NDKContextType } from 'contexts/NDKContext' import { Layout } from 'layout' -import { SearchPage } from 'pages/search' +import { SearchPage } from '../pages/search' import { AboutPage } from '../pages/about' -import { BlogsPage } from '../pages/blogs' import { GamesPage } from '../pages/games' import { HomePage } from '../pages/home' import { ModPage } from '../pages/mod' import { ModsPage } from '../pages/mods' import { ProfilePage } from '../pages/profile' +import { profileRouteLoader } from 'pages/profile/loader' import { SettingsPage } from '../pages/settings' import { SubmitModPage } from '../pages/submitMod' +import { GamePage } from '../pages/game' +import { NotFoundPage } from '../pages/404' +import { FeedLayout } from '../layout/feed' +import { FeedPage } from '../pages/feed' +import { NotificationsPage } from '../pages/notifications' import { WritePage } from '../pages/write' -import { GamePage } from 'pages/game' -import { NotFoundPage } from 'pages/404' -import { FeedLayout } from 'layout/feed' -import { FeedPage } from 'pages/feed' -import { NotificationsPage } from 'pages/notifications' +import { writeRouteAction } from '../pages/write/action' +import { BlogsPage } from 'pages/blogs' +import { blogsRouteLoader } from 'pages/blogs/loader' +import { BlogPage } from 'pages/blog' +import { blogRouteLoader } from 'pages/blog/loader' +import { blogRouteAction } from 'pages/blog/action' +import { blogReportRouteAction } from 'pages/blog/reportAction' export const appRoutes = { index: '/', @@ -25,7 +33,10 @@ export const appRoutes = { mods: '/mods', mod: '/mod/:naddr', about: '/about', - blog: '/blog', + blogs: '/blog', + blog: '/blog/:naddr', + blogEdit: '/blog/:naddr/edit', + blogReport_actionOnly: '/blog/:naddr/report', submitMod: '/submit-mod', editMod: '/edit-mod/:naddr', write: '/write', @@ -48,94 +59,119 @@ export const getModPageRoute = (eventId: string) => export const getModsEditPageRoute = (eventId: string) => appRoutes.editMod.replace(':naddr', eventId) +export const getBlogPageRoute = (eventId: string) => + appRoutes.blog.replace(':naddr', eventId) + export const getProfilePageRoute = (nprofile: string) => appRoutes.profile.replace(':nprofile', nprofile) -export const router = createBrowserRouter([ - { - element: , - children: [ - { - path: appRoutes.index, - element: - }, - { - path: appRoutes.games, - element: - }, - { - path: appRoutes.game, - element: - }, - { - path: appRoutes.mods, - element: - }, - { - path: appRoutes.mod, - element: - }, - { - path: appRoutes.about, - element: - }, - { - path: appRoutes.blog, - element: - }, - { - path: appRoutes.submitMod, - element: - }, - { - path: appRoutes.editMod, - element: - }, - { - path: appRoutes.write, - element: - }, - { - path: appRoutes.search, - element: - }, - { - path: appRoutes.settingsProfile, - element: - }, - { - path: appRoutes.settingsRelays, - element: - }, - { - path: appRoutes.settingsPreferences, - element: - }, - { - path: appRoutes.settingsAdmin, - element: - }, - { - path: appRoutes.profile, - element: - }, - { - element: , - children: [ - { - path: appRoutes.feed, - element: - }, - { - path: appRoutes.notifications, - element: - } - ] - }, - { - path: '*', - element: - } - ] - } -]) +export const routerWithNdkContext = (context: NDKContextType) => + createBrowserRouter([ + { + element: , + children: [ + { + path: appRoutes.index, + element: + }, + { + path: appRoutes.games, + element: + }, + { + path: appRoutes.game, + element: + }, + { + path: appRoutes.mods, + element: + }, + { + path: appRoutes.mod, + element: + }, + { + path: appRoutes.about, + element: + }, + { + path: appRoutes.blogs, + element: , + loader: blogsRouteLoader(context) + }, + { + path: appRoutes.blog, + element: , + loader: blogRouteLoader(context), + action: blogRouteAction(context), + errorElement: + }, + { + path: appRoutes.blogEdit, + element: , + loader: blogRouteLoader(context), + action: writeRouteAction(context), + errorElement: + }, + { + path: appRoutes.blogReport_actionOnly, + action: blogReportRouteAction(context) + }, + { + path: appRoutes.submitMod, + element: + }, + { + path: appRoutes.editMod, + element: + }, + { + path: appRoutes.write, + element: , + action: writeRouteAction(context) + }, + { + path: appRoutes.search, + element: + }, + { + path: appRoutes.settingsProfile, + element: + }, + { + path: appRoutes.settingsRelays, + element: + }, + { + path: appRoutes.settingsPreferences, + element: + }, + { + path: appRoutes.settingsAdmin, + element: + }, + { + path: appRoutes.profile, + element: , + loader: profileRouteLoader(context) + }, + { + element: , + children: [ + { + path: appRoutes.feed, + element: + }, + { + path: appRoutes.notifications, + element: + } + ] + }, + { + path: '*', + element: + } + ] + } + ]) diff --git a/src/styles/cardBlogs.css b/src/styles/cardBlogs.css index 1991b68..b075ef5 100644 --- a/src/styles/cardBlogs.css +++ b/src/styles/cardBlogs.css @@ -46,3 +46,20 @@ backdrop-filter: blur(5px); } +.cardBlogMainInsideTitle { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 2; + font-size: 20px; + line-height: 1.5; + color: rgba(255,255,255,0.75); + text-shadow: 0 0 8px rgba(0,0,0,0.25); +} + +.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagNSFW.IBMSMSMBSSTagsTagNSFWCard.IBMSMSMBSSTagsTagNSFWCardAlt { + position: absolute; + top: 10px; + right: 10px; + bottom: unset; +} diff --git a/src/styles/dotsSpinner.module.scss b/src/styles/dotsSpinner.module.scss new file mode 100644 index 0000000..772dfa8 --- /dev/null +++ b/src/styles/dotsSpinner.module.scss @@ -0,0 +1,18 @@ +.loading::after { + content: '.'; + animation: dots 1.5s steps(4, end) infinite; +} + +@keyframes dots { + 0%, + 20% { + content: '.\00a0\00a0'; + } + 40% { + content: '..\00a0'; + } + 60%, + 100% { + content: '...'; + } +} diff --git a/src/styles/post.css b/src/styles/post.css index faa4863..746f823 100644 --- a/src/styles/post.css +++ b/src/styles/post.css @@ -229,3 +229,12 @@ color: rgba(255,255,255,0.5); } +.dropdown.dropdownMain.dropdownMainBlogpost { + flex-grow: unset; + position: absolute; + top: 10px; + right: 10px; + background: #0000002e; + border-radius: 6px; + padding: 2px; +} \ No newline at end of file diff --git a/src/types/blog.ts b/src/types/blog.ts new file mode 100644 index 0000000..7b76549 --- /dev/null +++ b/src/types/blog.ts @@ -0,0 +1,42 @@ +export interface BlogForm { + title: string + content: string + image: string + summary: string + tags: string + nsfw: boolean +} + +export interface BlogDetails extends BlogForm { + id: string + author: string + published_at: number + edited_at: number + rTag: string + dTag: string + aTag: string + tTags: string[] +} + +export interface BlogEventSubmitForm extends Omit { + nsfw: string +} + +export interface BlogEventEditForm extends BlogEventSubmitForm { + dTag: string + rTag: string + published_at: string +} + +export interface BlogFormErrors extends Partial {} + +export interface BlogCardDetails extends BlogDetails { + naddr: string +} + +export interface BlogPageLoaderResult { + blog: Partial | undefined + latest: Partial[] + isAddedToNSFW: boolean + isBlocked: boolean +} diff --git a/src/types/index.ts b/src/types/index.ts index c589a79..8fe37df 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,3 +3,4 @@ export * from './modsFilter' export * from './nostr' export * from './user' export * from './zap' +export * from './blog' diff --git a/src/types/nostr.ts b/src/types/nostr.ts index 00e5ade..bdf46a4 100644 --- a/src/types/nostr.ts +++ b/src/types/nostr.ts @@ -7,3 +7,9 @@ export interface SignedEvent { id: string sig: string } + +export interface Addressable { + author: string + id: string + aTag: string +} diff --git a/src/utils/blog.ts b/src/utils/blog.ts new file mode 100644 index 0000000..089e058 --- /dev/null +++ b/src/utils/blog.ts @@ -0,0 +1,52 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk' +import { BlogCardDetails, BlogDetails } from 'types' +import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr' +import { kinds, nip19 } from 'nostr-tools' + +export const extractBlogDetails = (event: NDKEvent): Partial => { + const dTag = getFirstTagValue(event, 'd') + + // Check if the aTag exists on the blog + let aTag = getFirstTagValue(event, 'a') + + // Create aTag from components if aTag is not included + if (typeof aTag === 'undefined' && event.pubkey && dTag) { + aTag = `${kinds.LongFormArticle}:${event.pubkey}:${dTag}` + } + + return { + title: getFirstTagValue(event, 'title'), + content: event.content, + summary: getFirstTagValue(event, 'summary'), + image: getFirstTagValue(event, 'image'), + // Check L label namespace for content warning or nsfw (backwards compatibility) + nsfw: + getFirstTagValue(event, 'L') === 'content-warning' || + getFirstTagValue(event, 'nsfw') === 'true', + id: event.id, + author: event.pubkey, + published_at: getFirstTagValueAsInt(event, 'published_at'), + edited_at: event.created_at, + rTag: getFirstTagValue(event, 'r') || 'N/A', + dTag: dTag, + aTag: aTag, + tTags: getTagValues(event, 't') || [] + } +} + +export const extractBlogCardDetails = ( + event: NDKEvent +): Partial => { + const blogDetails = extractBlogDetails(event) + + return { + ...blogDetails, + naddr: blogDetails.dTag + ? nip19.naddrEncode({ + identifier: blogDetails.dTag, + kind: kinds.LongFormArticle, + pubkey: event.pubkey + }) + : undefined + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 06e7ca4..3391eb9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,4 @@ export * from './utils' export * from './zap' export * from './localStorage' export * from './consts' +export * from './blog' diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 130d023..772c37b 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -65,6 +65,38 @@ export const getTagValue = ( return null } +export const getTagValues = ( + event: Event | NDKEvent, + tagIdentifier: string +): string[] | null => { + // Find the tag in the event's tags array where the first element matches the tagIdentifier. + const tags = event.tags.filter((item) => item[0] === tagIdentifier) + + // If a matching tag is found, return the rest of the elements in the tag (i.e., the values). + if (tags && tags.length) { + return tags.map((item) => item[1]) // Returning only the values + } + + // Return null if no matching tag is found. + return null +} + +export const getFirstTagValue = ( + event: Event | NDKEvent, + tagIdentifier: string +) => { + const tags = getTagValues(event, tagIdentifier) + return tags && tags.length ? tags[0] : undefined +} + +export const getFirstTagValueAsInt = ( + event: Event | NDKEvent, + tagIdentifier: string +) => { + const value = getFirstTagValue(event, tagIdentifier) + return value ? parseInt(value, 10) : undefined +} + /** * @param hexKey hex private or public key * @returns whether or not is key valid diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 6c904e0..4658496 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -146,3 +146,13 @@ export const scrollIntoView = (el: HTMLElement | null) => { }, 100) } } + +export const parseFormData = (formData: FormData) => { + const result: Partial = {} + + formData.forEach( + (value, key) => ((result as Record)[key] = value as string) + ) + + return result +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 1f3c47c..6363e3e 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -6,6 +6,7 @@ interface ImportMetaEnv { readonly VITE_REPORTING_NPUB: string readonly VITE_FALLBACK_MOD_IMAGE: string readonly VITE_FALLBACK_GAME_IMAGE: string + readonly VITE_BLOG_NPUBS: string // more env variables... }