diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 1220b122..3063d95d 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -29,14 +29,14 @@ jobs: runs-on: ubuntu-latest if: github.event.deployment_status.state == 'success' steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - - name: Install dependencies - run: npm ci - - name: Install Playwright - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - env: - BASE_URL: ${{ github.event.deployment_status.target_url }} + - name: Install dependencies + run: npm ci + - name: Install Playwright + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + env: + BASE_URL: ${{ github.event.deployment_status.target_url }} diff --git a/biome.json b/biome.json index d279c479..7b211ccc 100644 --- a/biome.json +++ b/biome.json @@ -1,21 +1,8 @@ { "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", "files": { - "include": [ - "*.tsx", - "*.ts", - "*.js", - "*.json" - ], - "ignore": [ - "./dist", - "./node_modules", - ".husky", - "docs", - "gen", - "*.gen.ts", - "*.d.ts" - ] + "include": ["*.tsx", "*.ts", "*.js", "*.json"], + "ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"] }, "vcs": { "defaultBranch": "dev", @@ -23,19 +10,13 @@ }, "organizeImports": { "enabled": true, - "ignore": [ - "./api", - "./gen" - ] + "ignore": ["./api", "./gen"] }, "formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 108, - "ignore": [ - "./src/graphql/schema", - "./gen" - ] + "ignore": ["./src/graphql/schema", "./gen"] }, "javascript": { "formatter": { @@ -48,13 +29,7 @@ } }, "linter": { - "ignore": [ - "*.scss", - "*.md", - ".DS_Store", - "*.svg", - "*.d.ts" - ], + "ignore": ["*.scss", "*.md", ".DS_Store", "*.svg", "*.d.ts"], "enabled": true, "rules": { "all": true, diff --git a/package-lock.json b/package-lock.json index d3ea3271..bf39951b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,9 +34,8 @@ "@solid-primitives/memo": "1.2.4", "@solid-primitives/pagination": "0.2.10", "@solid-primitives/share": "2.0.4", - "@solid-primitives/storage": "1.3.9", - "@solid-primitives/upload": "0.0.110", - "@solidjs/meta": "0.29.1", + "@solid-primitives/storage": "^3.5.0", + "@solid-primitives/upload": "0.0.115", "@thisbeyond/solid-select": "0.14.0", "@tiptap/core": "2.2.3", "@tiptap/extension-blockquote": "2.2.3", @@ -95,9 +94,9 @@ "prosemirror-history": "1.3.2", "prosemirror-trailing-node": "2.0.7", "prosemirror-view": "1.32.7", - "rollup": "4.11.0", + "rollup": "4.17.2", "sass": "1.69.5", - "solid-js": "1.8.15", + "solid-js": "1.8.17", "solid-popper": "0.3.0", "solid-tiptap": "0.7.0", "solid-transition-group": "0.2.3", @@ -111,7 +110,7 @@ "typograf": "7.3.0", "uniqolor": "1.1.0", "vike": "0.4.148", - "vite": "5.2.10", + "vite": "5.2.11", "vite-plugin-mkcert": "^1.17.3", "vite-plugin-node-polyfills": "0.21.0", "vite-plugin-sass-dts": "^1.3.17", @@ -1204,9 +1203,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.2.tgz", - "integrity": "sha512-6Skx9N47inLQzYi9RKgJ7PBnUnaHnMe/imqX43cOcJjZtfMnQLxEvfM2Eyo7gChkwrZlwc+VbA4huFRjw2fsYA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.3.tgz", + "integrity": "sha512-ogFQI+fpXftr+tiahA6bIXwZ7CSikygASdqMtH07J2cUzrpjyTMVc9Y97v23c7/tL1xCZhM+W9k4hYIBm7Q6cQ==", "dev": true, "hasInstallScript": true, "bin": { @@ -1220,20 +1219,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.7.2", - "@biomejs/cli-darwin-x64": "1.7.2", - "@biomejs/cli-linux-arm64": "1.7.2", - "@biomejs/cli-linux-arm64-musl": "1.7.2", - "@biomejs/cli-linux-x64": "1.7.2", - "@biomejs/cli-linux-x64-musl": "1.7.2", - "@biomejs/cli-win32-arm64": "1.7.2", - "@biomejs/cli-win32-x64": "1.7.2" + "@biomejs/cli-darwin-arm64": "1.7.3", + "@biomejs/cli-darwin-x64": "1.7.3", + "@biomejs/cli-linux-arm64": "1.7.3", + "@biomejs/cli-linux-arm64-musl": "1.7.3", + "@biomejs/cli-linux-x64": "1.7.3", + "@biomejs/cli-linux-x64-musl": "1.7.3", + "@biomejs/cli-win32-arm64": "1.7.3", + "@biomejs/cli-win32-x64": "1.7.3" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.2.tgz", - "integrity": "sha512-CrldIueHivWEWmeTkK8bTXajeX53F8i2Rrkkt8cPZyMtzkrwxf8Riq4a/jz3SQBHkxHFT4TqGbSTNMXe3X1ogA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.3.tgz", + "integrity": "sha512-eDvLQWmGRqrPIRY7AIrkPHkQ3visEItJKkPYSHCscSDdGvKzYjmBJwG1Gu8+QC5ed6R7eiU63LEC0APFBobmfQ==", "cpu": [ "arm64" ], @@ -1247,9 +1246,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.2.tgz", - "integrity": "sha512-UELnLJuJOsTL9meArvn8BtiXDURyPil2Ej9me2uVpEvee8UQdqd/bssP5we400OWShlL1AAML4fn6d2WX5332g==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.3.tgz", + "integrity": "sha512-JXCaIseKRER7dIURsVlAJacnm8SG5I0RpxZ4ya3dudASYUc68WGl4+FEN03ABY3KMIq7hcK1tzsJiWlmXyosZg==", "cpu": [ "x64" ], @@ -1263,9 +1262,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.2.tgz", - "integrity": "sha512-Z1CSGQE6fHz55gkiFHv9E8wEAaSUd7dHSRaxSCBa7utonHqpIeMbvj3Evm1w0WfGLFDtRXLV1fTfEdM0FMTOhA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.3.tgz", + "integrity": "sha512-phNTBpo7joDFastnmZsFjYcDYobLTx4qR4oPvc9tJ486Bd1SfEVPHEvJdNJrMwUQK56T+TRClOQd/8X1nnjA9w==", "cpu": [ "arm64" ], @@ -1279,9 +1278,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.2.tgz", - "integrity": "sha512-kKYZiem7Sj7wI0dpVxJlK7C+TFQwzO/ctufIGXGJAyEmUe9vEKSzV8CXpv+JIRiTWyqaZJ4K+eHz4SPdPCv05w==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.3.tgz", + "integrity": "sha512-c8AlO45PNFZ1BYcwaKzdt46kYbuP6xPGuGQ6h4j3XiEDpyseRRUy/h+6gxj07XovmyxKnSX9GSZ6nVbZvcVUAw==", "cpu": [ "arm64" ], @@ -1295,9 +1294,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.2.tgz", - "integrity": "sha512-vXXyox8/CQijBxAu0+r8FfSO7JlC4tob3PbaFda8gPJFRz2uFJw39HtxVUwbTV1EcU6wSPh4SiRu5sZfP1VHrQ==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.3.tgz", + "integrity": "sha512-vnedYcd5p4keT3iD48oSKjOIRPYcjSNNbd8MO1bKo9ajg3GwQXZLAH+0Cvlr+eMsO67/HddWmscSQwTFrC/uPA==", "cpu": [ "x64" ], @@ -1311,9 +1310,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.2.tgz", - "integrity": "sha512-x10LpGMepDrLS+h2TZ6/T7egpHjGKtiI4GuShNylmBQJWfTotbFf9eseHggrqJ4WZf9yrGoVYrtbxXftuB95sQ==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.3.tgz", + "integrity": "sha512-UdEHKtYGWEX3eDmVWvQeT+z05T9/Sdt2+F/7zmMOFQ7boANeX8pcO6EkJPK3wxMudrApsNEKT26rzqK6sZRTRA==", "cpu": [ "x64" ], @@ -1327,9 +1326,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.2.tgz", - "integrity": "sha512-kRXdlKzcU7INf6/ldu0nVmkOgt7bKqmyXRRCUqqaJfA32+9InTbkD8tGrHZEVYIWr+eTuKcg16qZVDsPSDFZ8g==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.3.tgz", + "integrity": "sha512-unNCDqUKjujYkkSxs7gFIfdasttbDC4+z0kYmcqzRk6yWVoQBL4dNLcCbdnJS+qvVDNdI9rHp2NwpQ0WAdla4Q==", "cpu": [ "arm64" ], @@ -1343,9 +1342,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.2.tgz", - "integrity": "sha512-qHTtpAs+CNglAAuaTy09htoqUhrQyd3nd0aGTuLNqD10h1llMVi8WFZfoa+e5MuDSfYtMK6nW2Tbf6WgzzR1Qw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.3.tgz", + "integrity": "sha512-ZmByhbrnmz/UUFYB622CECwhKIPjJLLPr5zr3edhu04LzbfcOrz16VYeNq5dpO1ADG70FORhAJkaIGdaVBG00w==", "cpu": [ "x64" ], @@ -1392,9 +1391,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz", - "integrity": "sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz", + "integrity": "sha512-xI/tL2zxzEbESvnSxwFgwvy5HS00oCXxL4MLs6HUiDcYfwowsoQaABKxUElp1ARITrINzBnsECOc1q0eg2GOrA==", "dev": true, "funding": [ { @@ -1410,13 +1409,13 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.2.4" + "@csstools/css-tokenizer": "^2.3.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz", - "integrity": "sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.1.tgz", + "integrity": "sha512-iMNHTyxLbBlWIfGtabT157LH9DUx9X8+Y3oymFEuMj8HNc+rpE3dPFGFgHjpKfjeFDjLjYIAIhXPGvS2lKxL9g==", "dev": true, "funding": [ { @@ -1433,9 +1432,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz", - "integrity": "sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA==", + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.11.tgz", + "integrity": "sha512-uox5MVhvNHqitPP+SynrB1o8oPxPMt2JLgp5ghJOWf54WGQ5OKu47efne49r1SWqs3wRP8xSWjnO9MBKxhB1dA==", "dev": true, "funding": [ { @@ -1451,8 +1450,8 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.6.1", - "@csstools/css-tokenizer": "^2.2.4" + "@csstools/css-parser-algorithms": "^2.6.3", + "@csstools/css-tokenizer": "^2.3.1" } }, "node_modules/@csstools/selector-specificity": { @@ -1487,22 +1486,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/android-arm": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", @@ -3632,9 +3615,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.11.0.tgz", - "integrity": "sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", "cpu": [ "arm" ], @@ -3645,9 +3628,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.11.0.tgz", - "integrity": "sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", "cpu": [ "arm64" ], @@ -3658,9 +3641,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.11.0.tgz", - "integrity": "sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", "cpu": [ "arm64" ], @@ -3671,9 +3654,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.11.0.tgz", - "integrity": "sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", "cpu": [ "x64" ], @@ -3684,9 +3667,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.11.0.tgz", - "integrity": "sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", "cpu": [ "arm" ], @@ -3710,9 +3693,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.11.0.tgz", - "integrity": "sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", "cpu": [ "arm64" ], @@ -3723,9 +3706,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.11.0.tgz", - "integrity": "sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", "cpu": [ "arm64" ], @@ -3749,9 +3732,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.11.0.tgz", - "integrity": "sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", "cpu": [ "riscv64" ], @@ -3775,9 +3758,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.11.0.tgz", - "integrity": "sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", "cpu": [ "x64" ], @@ -3788,9 +3771,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.11.0.tgz", - "integrity": "sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", "cpu": [ "x64" ], @@ -3801,9 +3784,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.11.0.tgz", - "integrity": "sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", "cpu": [ "arm64" ], @@ -3814,9 +3797,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.11.0.tgz", - "integrity": "sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", "cpu": [ "ia32" ], @@ -3827,9 +3810,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.11.0.tgz", - "integrity": "sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", "cpu": [ "x64" ], @@ -4072,15 +4055,24 @@ } }, "node_modules/@solid-primitives/storage": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-1.3.9.tgz", - "integrity": "sha512-ysJSIycmToQD8Hpt4jpIlh7U8EuYdpQwkamppng3g93E5f6RZVPCzYmRZ+ckRN2cNLFpAuTEqZx7OBRh3PBWFQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-3.5.0.tgz", + "integrity": "sha512-AqU3vrXz8XlxOgJIiP+oQxE/vFchGf4Qe7E5Xfa02DJdF9rD8CtiTmVZDBU08ViS7g0Nwc4IpStHvVO0jBMalQ==", "dev": true, "dependencies": { - "@solid-primitives/utils": "^6.0.0" + "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { + "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" + }, + "peerDependenciesMeta": { + "@tauri-apps/plugin-store": { + "optional": true + }, + "solid-start": { + "optional": true + } } }, "node_modules/@solid-primitives/transition-group": { @@ -4093,26 +4085,17 @@ } }, "node_modules/@solid-primitives/upload": { - "version": "0.0.110", - "resolved": "https://registry.npmjs.org/@solid-primitives/upload/-/upload-0.0.110.tgz", - "integrity": "sha512-YQZGogXzc77c/3hxDoxGi78FkvQQfUbElbdSPn+E0GRl21XMuJbD/QKQKNXm7KyxX+cMTwLnQYCoqfRXcgHMIA==", + "version": "0.0.115", + "resolved": "https://registry.npmjs.org/@solid-primitives/upload/-/upload-0.0.115.tgz", + "integrity": "sha512-CWTXz28mmRGvZV90IzViNtBAKC6cnO2WSNb3UjvbkPRrtxlHrj/ewZwLRhuLSu6JzdY/c+rHi1j24v7H0SrdXg==", "dev": true, "dependencies": { - "@solid-primitives/utils": "^5.5.1" + "@solid-primitives/utils": "^6.2.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, - "node_modules/@solid-primitives/upload/node_modules/@solid-primitives/utils": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-5.5.2.tgz", - "integrity": "sha512-L52ig3eHKU6CqbPCKJIb4lweBuINHBOERcE1duApyKozEN8+zCqEKwD1Qo9ljKeEzJTBGWClxNpwEiNTUWTGvg==", - "dev": true, - "peerDependencies": { - "solid-js": "^1.6.12" - } - }, "node_modules/@solid-primitives/utils": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz", @@ -4122,15 +4105,6 @@ "solid-js": "^1.6.12" } }, - "node_modules/@solidjs/meta": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@solidjs/meta/-/meta-0.29.1.tgz", - "integrity": "sha512-qtrBYCnRRuzyvBg/u/SRO/2fM5r6DT1YKf+2W1RZhveMoeXHbZpWIrXjgpLFRHJLn6cqAGqrIzu42qS2o+1hKQ==", - "dev": true, - "peerDependencies": { - "solid-js": ">=1.8.4" - } - }, "node_modules/@thisbeyond/solid-select": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.14.0.tgz", @@ -4630,9 +4604,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", - "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", + "version": "20.12.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz", + "integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -5454,9 +5428,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001615", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", - "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", + "version": "1.0.30001616", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz", + "integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==", "dev": true, "funding": [ { @@ -6290,9 +6264,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.756", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz", - "integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==", + "version": "1.4.757", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.757.tgz", + "integrity": "sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==", "dev": true }, "node_modules/elliptic": { @@ -10796,9 +10770,9 @@ } }, "node_modules/prosemirror-model": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.20.0.tgz", - "integrity": "sha512-q7AY7vMjKYqDCeoedgUiAgrLabliXxndJuuFmcmc2+YU1SblvnOiG2WEACF2lwAZsMlfLpiAilA3L+TWlDqIsQ==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.21.0.tgz", + "integrity": "sha512-zLpS1mVCZLA7VTp82P+BfMiYVPcX1/z0Mf3gsjKZtzMWubwn2pN7CceMV0DycjlgE5JeXPR7UF4hJPbBV98oWA==", "dev": true, "dependencies": { "orderedmap": "^2.0.0" @@ -10880,12 +10854,12 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz", - "integrity": "sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz", + "integrity": "sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==", "dev": true, "dependencies": { - "prosemirror-model": "^1.0.0" + "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { @@ -11193,9 +11167,9 @@ } }, "node_modules/rollup": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.11.0.tgz", - "integrity": "sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -11208,19 +11182,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.11.0", - "@rollup/rollup-android-arm64": "4.11.0", - "@rollup/rollup-darwin-arm64": "4.11.0", - "@rollup/rollup-darwin-x64": "4.11.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.11.0", - "@rollup/rollup-linux-arm64-gnu": "4.11.0", - "@rollup/rollup-linux-arm64-musl": "4.11.0", - "@rollup/rollup-linux-riscv64-gnu": "4.11.0", - "@rollup/rollup-linux-x64-gnu": "4.11.0", - "@rollup/rollup-linux-x64-musl": "4.11.0", - "@rollup/rollup-win32-arm64-msvc": "4.11.0", - "@rollup/rollup-win32-ia32-msvc": "4.11.0", - "@rollup/rollup-win32-x64-msvc": "4.11.0", + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", "fsevents": "~2.3.2" } }, @@ -11550,13 +11527,13 @@ } }, "node_modules/solid-js": { - "version": "1.8.15", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.15.tgz", - "integrity": "sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==", + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.17.tgz", + "integrity": "sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==", "dev": true, "dependencies": { "csstype": "^3.1.0", - "seroval": "^1.0.3", + "seroval": "^1.0.4", "seroval-plugins": "^1.0.3" } }, @@ -12530,9 +12507,9 @@ } }, "node_modules/vite": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", - "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==", + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", "dev": true, "dependencies": { "esbuild": "^0.20.1", @@ -12660,6 +12637,22 @@ } } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vite/node_modules/@esbuild/android-arm": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", @@ -13012,175 +13005,6 @@ "node": ">=12" } }, - "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/vite/node_modules/esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", @@ -13233,41 +13057,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/vite/node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", - "fsevents": "~2.3.2" - } - }, "node_modules/vitefu": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", diff --git a/package.json b/package.json index 76af1218..35efc4a1 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ "deploy": "graphql-codegen && npm run typecheck && vite build && vercel", "dev": "vite", "e2e": "npx playwright test --project=chromium", - "fix": "npm run lint:code:fix && stylelint **/*.{scss,css} --fix", + "fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix", "format": "npx @biomejs/biome format src/. --write", "hygen": "HYGEN_TMPLS=gen hygen", "postinstall": "npm run codegen && npx patch-package", "check:code": "npx @biomejs/biome check src --log-kind=compact --verbose", - "check:code:fix": "npx @biomejs/biome lint src --log-kind=compact", + "check:code:fix": "npx @biomejs/biome check . --apply", "lint": "npm run lint:code && stylelint **/*.{scss,css}", "lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose", "lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose", @@ -52,9 +52,8 @@ "@solid-primitives/memo": "1.2.4", "@solid-primitives/pagination": "0.2.10", "@solid-primitives/share": "2.0.4", - "@solid-primitives/storage": "1.3.9", - "@solid-primitives/upload": "0.0.110", - "@solidjs/meta": "0.29.1", + "@solid-primitives/storage": "^3.5.0", + "@solid-primitives/upload": "0.0.115", "@thisbeyond/solid-select": "0.14.0", "@tiptap/core": "2.2.3", "@tiptap/extension-blockquote": "2.2.3", @@ -113,9 +112,9 @@ "prosemirror-history": "1.3.2", "prosemirror-trailing-node": "2.0.7", "prosemirror-view": "1.32.7", - "rollup": "4.11.0", + "rollup": "4.17.2", "sass": "1.69.5", - "solid-js": "1.8.15", + "solid-js": "1.8.17", "solid-popper": "0.3.0", "solid-tiptap": "0.7.0", "solid-transition-group": "0.2.3", @@ -129,7 +128,7 @@ "typograf": "7.3.0", "uniqolor": "1.1.0", "vike": "0.4.148", - "vite": "5.2.10", + "vite": "5.2.11", "vite-plugin-mkcert": "^1.17.3", "vite-plugin-node-polyfills": "0.21.0", "vite-plugin-sass-dts": "^1.3.17", @@ -141,7 +140,5 @@ "y-prosemirror": "1.2.2", "yjs": "13.6.12" }, - "trustedDependencies": [ - "@biomejs/biome" - ] + "trustedDependencies": ["@biomejs/biome"] } diff --git a/public/icons/logout.svg b/public/icons/logout.svg new file mode 100644 index 00000000..a28a25c8 --- /dev/null +++ b/public/icons/logout.svg @@ -0,0 +1,4 @@ + + + diff --git a/public/icons/profile.svg b/public/icons/profile.svg new file mode 100644 index 00000000..23c7e580 --- /dev/null +++ b/public/icons/profile.svg @@ -0,0 +1,4 @@ + + + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 2690613b..dea79bdb 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -397,7 +397,7 @@ "Top authors": "Authors rating", "Top commented": "Most commented", "Top discussed": "Top discussed", - "Top month articles": "Top of the month", + "Top month": "Top of the month", "Top rated": "Popular", "Top recent": "Most recent", "Top topics": "Interesting topics", @@ -448,6 +448,7 @@ "Write your colleagues name or email": "Write your colleague's name or email", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats", "You can now login using your new password": "Теперь вы можете входить с помощью нового пароля", + "You can't edit this post": "You can't edit this post", "You were successfully authorized": "You were successfully authorized", "You ll be able to participate in discussions, rate others' comments and learn about new responses": "You ll be able to participate in discussions, rate others' comments and learn about new responses", "You've confirmed email": "You've confirmed email", @@ -530,5 +531,13 @@ "It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password", "Restore password": "Restore password", "Subscribing...": "Subscribing...", - "Unsubscribing...": "Unsubscribing..." + "Unsubscribing...": "Unsubscribing...", + "Login and security": "Login and security", + "Settings for account, email, password and login methods.": "Settings for account, email, password and login methods.", + "Current password": "Current password", + "Confirm your new password": "Confirm your new password", + "Connect": "Connect", + "Incorrect old password": "Incorrect old password", + "Repeat new password": "Repeat new password", + "Incorrect new password confirm": "Incorrect new password confirm" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 2dc94b46..f9596f64 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -418,7 +418,7 @@ "Top authors": "Рейтинг авторов", "Top commented": "Самое комментируемое", "Top discussed": "Обсуждаемое", - "Top month articles": "Лучшие материалы месяца", + "Top month": "Лучшее за месяц", "Top rated": "Популярное", "Top recent": "Самое новое", "Top topics": "Интересные темы", @@ -471,6 +471,7 @@ "You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac", "You can now login using your new password": "Теперь вы можете входить с помощью нового пароля", "You was successfully authorized": "Вы были успешно авторизованы", + "You can't edit this post": "Вы не можете редактировать этот материал", "You ll be able to participate in discussions, rate others' comments and learn about new responses": "Вы сможете участвовать в обсуждениях, оценивать комментарии других и узнавать о новых ответах", "You've confirmed email": "Вы подтвердили почту", "You've reached a non-existed page": "Вы попали на несуществующую страницу", @@ -557,5 +558,13 @@ "It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля", "Restore password": "Восстановить пароль", "Subscribing...": "Подписываем...", - "Unsubscribing...": "Отписываем..." + "Unsubscribing...": "Отписываем...", + "Login and security": "Вход и безопасность", + "Settings for account, email, password and login methods.": "Настройки аккаунта, почты, пароля и способов входа.", + "Current password": "Текущий пароль", + "Confirm your new password": "Подтвердите новый пароль", + "Connect": "Привязать", + "Incorrect old password": "Старый пароль не верен", + "Repeat new password": "Повторите новый пароль", + "Incorrect new password confirm": "Неверное подтверждение нового пароля" } diff --git a/src/components/App.tsx b/src/components/App.tsx index 57b218f1..00b5dfa9 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,8 +1,8 @@ import type { PageProps, RootSearchParams } from '../pages/types' -import { Meta, MetaProvider } from '@solidjs/meta' import { Component, createEffect, createMemo } from 'solid-js' import { Dynamic } from 'solid-js/web' +import { Meta, MetaProvider } from '../context/meta' import { ConfirmProvider } from '../context/confirm' import { ConnectProvider } from '../context/connect' @@ -12,6 +12,7 @@ import { InboxProvider } from '../context/inbox' import { LocalizeProvider } from '../context/localize' import { MediaQueryProvider } from '../context/mediaQuery' import { NotificationsProvider } from '../context/notifications' +import { SeenProvider } from '../context/seen' import { SessionProvider } from '../context/session' import { SnackbarProvider } from '../context/snackbar' import { TopicsProvider } from '../context/topics' @@ -40,16 +41,12 @@ import { InboxPage } from '../pages/inbox.page' import { HomePage } from '../pages/index.page' import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page' import { ProfileSettingsPage } from '../pages/profile/profileSettings.page' -//TODO: ProfileSubscriptionsPage - garbage code? import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page' import { SearchPage } from '../pages/search.page' import { TopicPage } from '../pages/topic.page' import { ROUTES, useRouter } from '../stores/router' import { MODALS, showModal } from '../stores/ui' -// TODO: lazy load -// const SomePage = lazy(() => import('./Pages/SomePage')) - const pagesMap: Record> = { author: AuthorPage, authorComments: AuthorPage, @@ -119,21 +116,23 @@ export const App = (props: Props) => { - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/src/components/Article/Comment/Comment.tsx b/src/components/Article/Comment/Comment.tsx index d66e1767..5e94c4a8 100644 --- a/src/components/Article/Comment/Comment.tsx +++ b/src/components/Article/Comment/Comment.tsx @@ -127,7 +127,7 @@ export const Comment = (props: Props) => {
  • props.lastSeen, + [styles.isNew]: props.lastSeen > (props.comment.updated_at || props.comment.created_at), })} > diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 60054831..d899be8b 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -11,6 +11,7 @@ import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import { Comment } from './Comment' +import { useSeen } from '../../context/seen' import styles from './Article.module.scss' const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor')) @@ -29,7 +30,7 @@ export const CommentsTree = (props: Props) => { const [newReactions, setNewReactions] = createSignal([]) const [clearEditor, setClearEditor] = createSignal(false) const [clickedReplyId, setClickedReplyId] = createSignal() - const { reactionEntities, createReaction } = useReactions() + const { reactionEntities, createReaction, loadReactionsBy } = useReactions() const comments = createMemo(() => Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'), @@ -48,27 +49,28 @@ export const CommentsTree = (props: Props) => { } return newSortedComments }) - - const dateFromLocalStorage = Number.parseInt(localStorage.getItem(`${props.shoutSlug}`)) + const { seen } = useSeen() + const shoutLastSeen = createMemo(() => seen()[props.shoutSlug] ?? 0) const currentDate = new Date() const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`) onMount(() => { - if (!dateFromLocalStorage) { + if (!shoutLastSeen()) { setCookie() - } else if (currentDate.getTime() > dateFromLocalStorage) { + } else if (currentDate.getTime() > shoutLastSeen()) { const newComments = comments().filter((c) => { if (c.reply_to || c.created_by.slug === author()?.slug) { return } - const created = c.created_at - return created > dateFromLocalStorage + return (c.updated_at || c.created_at) > shoutLastSeen() }) setNewReactions(newComments) setCookie() } }) + const [posting, setPosting] = createSignal(false) const handleSubmitComment = async (value: string) => { + setPosting(true) try { await createReaction({ kind: ReactionKind.Comment, @@ -76,10 +78,12 @@ export const CommentsTree = (props: Props) => { shout: props.shoutId, }) setClearEditor(true) + await loadReactionsBy({ by: { shout: props.shoutSlug } }) } catch (error) { console.error('[handleCreate reaction]:', error) } setClearEditor(false) + setPosting(false) } return ( @@ -130,7 +134,7 @@ export const CommentsTree = (props: Props) => { comment={reaction} clickedReply={(id) => setClickedReplyId(id)} clickedReplyId={clickedReplyId()} - lastSeen={dateFromLocalStorage} + lastSeen={shoutLastSeen()} /> )} @@ -157,6 +161,7 @@ export const CommentsTree = (props: Props) => { placeholder={t('Write a comment...')} onSubmit={(value) => handleSubmitComment(value)} setClear={clearEditor()} + isPosting={posting()} /> diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 25001f38..3cea62c2 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -2,11 +2,11 @@ import type { Author, Shout, Topic } from '../../graphql/schema/core.gen' import { getPagePath } from '@nanostores/router' import { createPopper } from '@popperjs/core' -import { Link, Meta } from '@solidjs/meta' import { clsx } from 'clsx' import { install } from 'ga-gtag' import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' import { isServer } from 'solid-js/web' +import { Link, Meta } from '../../context/meta' import { useLocalize } from '../../context/localize' import { useReactions } from '../../context/reactions' @@ -38,6 +38,7 @@ import { CommentsTree } from './CommentsTree' import { SharePopup, getShareUrl } from './SharePopup' import { ShoutRatingControl } from './ShoutRatingControl' +import { useSeen } from '../../context/seen' import stylesHeader from '../Nav/Header/Header.module.scss' import styles from './Article.module.scss' @@ -75,7 +76,8 @@ export const FullArticle = (props: Props) => { const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const { t, formatDate, lang } = useLocalize() - const { author, session, isAuthenticated, requireAuthentication } = useSession() + const { author, session, requireAuthentication } = useSession() + const { addSeen } = useSeen() const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000))) @@ -302,6 +304,7 @@ export const FullArticle = (props: Props) => { onMount(async () => { install('G-LQ4B87H8C2') await loadReactionsBy({ by: { shout: props.article.slug } }) + addSeen(props.article.slug) setIsReactionsLoaded(true) document.title = props.article.title window?.addEventListener('resize', updateIframeSizes) @@ -535,7 +538,7 @@ export const FullArticle = (props: Props) => { {(triggerRef: (el) => void) => ( - +
    diff --git a/src/components/Article/SharePopup.tsx b/src/components/Article/SharePopup.tsx index a5fe254f..2983bea7 100644 --- a/src/components/Article/SharePopup.tsx +++ b/src/components/Article/SharePopup.tsx @@ -28,7 +28,7 @@ export const SharePopup = (props: SharePopupProps) => { }) return ( - setIsVisible(value)}> + setIsVisible(value)}> { - const { isAuthenticated, isSessionLoaded } = useSession() + const { author, isSessionLoaded } = useSession() const { changeSearchParams } = useRouter() createEffect(() => { @@ -20,7 +20,7 @@ export const AuthGuard = (props: Props) => { return } if (isSessionLoaded()) { - if (isAuthenticated()) { + if (author()?.id) { hideModal() } else { changeSearchParams( @@ -37,5 +37,5 @@ export const AuthGuard = (props: Props) => { } }) - return {props.children} + return {props.children} } diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 19a2deb3..b8260e2a 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -54,7 +54,7 @@ export const AuthorBadge = (props: Props) => { requireAuthentication(() => { openPage(router, 'inbox') changeSearchParams({ - initChat: props.author.id.toString(), + initChat: props.author?.id.toString(), }) }, 'discussions') } @@ -118,6 +118,9 @@ export const AuthorBadge = (props: Props) => { 0}>
    {t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
    + 0}> +
    {t('CommentsWithCount', { count: props.author.stat?.comments ?? 0 })}
    +
    0}>
    {t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}
    diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index c3b1ccd3..f1b58fef 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -65,7 +65,7 @@ export const AuthorCard = (props: Props) => { requireAuthentication(() => { openPage(router, 'inbox') changeSearchParams({ - initChat: props.author.id.toString(), + initChat: props.author?.id.toString(), }) }, 'discussions') } diff --git a/src/components/Discours/Hero.tsx b/src/components/Discours/Hero.tsx index 105300cc..872db08c 100644 --- a/src/components/Discours/Hero.tsx +++ b/src/components/Discours/Hero.tsx @@ -1,7 +1,7 @@ import { useLocalize } from '../../context/localize' import { useRouter } from '../../stores/router' import { showModal } from '../../stores/ui' -import { AuthModalSearchParams } from '../Nav/AuthModal/types' +import type { AuthModalSearchParams } from '../Nav/AuthModal/types' import styles from './Hero.module.scss' diff --git a/src/components/Draft/Draft.tsx b/src/components/Draft/Draft.tsx index cfbc9b0c..b66b9c1a 100644 --- a/src/components/Draft/Draft.tsx +++ b/src/components/Draft/Draft.tsx @@ -60,7 +60,7 @@ export const Draft = (props: Props) => {
    {t('Edit')} diff --git a/src/components/Editor/AudioUploader/AudioUploader.tsx b/src/components/Editor/AudioUploader/AudioUploader.tsx index 329dac3e..b5cc4954 100644 --- a/src/components/Editor/AudioUploader/AudioUploader.tsx +++ b/src/components/Editor/AudioUploader/AudioUploader.tsx @@ -7,6 +7,7 @@ import { composeMediaItems } from '../../../utils/composeMediaItems' import { AudioPlayer } from '../../Article/AudioPlayer' import { DropArea } from '../../_shared/DropArea' +// import { Buffer } from 'node:buffer' import styles from './AudioUploader.module.scss' window.Buffer = Buffer diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 950aa355..26b79f75 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -151,7 +151,7 @@ export const Editor = (props: Props) => { } showSnackbar({ body: t('Uploading image') }) - const result = await handleImageUpload(uplFile) + const result = await handleImageUpload(uplFile, session()?.access_token) editor() .chain() diff --git a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx index 5c349d72..5e3bcfdc 100644 --- a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx +++ b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx @@ -28,10 +28,11 @@ const embedData = (data) => { const result: { src: string; width?: string; height?: string } = { src: '' } - // biome-ignore lint/style/useForOf: for (let i = 0; i < attributes.length; i++) { - const attribute = attributes[i] - result[attribute.name] = attribute.value + const attribute = attributes.item(i) + if (attribute) { + result[attribute.name] = attribute.value + } } return result diff --git a/src/components/Editor/Panel/Panel.tsx b/src/components/Editor/Panel/Panel.tsx index fe96f1e7..aaf7bd60 100644 --- a/src/components/Editor/Panel/Panel.tsx +++ b/src/components/Editor/Panel/Panel.tsx @@ -23,8 +23,16 @@ type Props = { export const Panel = (props: Props) => { const { t } = useLocalize() - const { isEditorPanelVisible, wordCounter, editorRef, form, toggleEditorPanel, saveShout, publishShout } = - useEditorContext() + const { + isEditorPanelVisible, + wordCounter, + editorRef, + form, + toggleEditorPanel, + saveShout, + saveDraft, + publishShout, + } = useEditorContext() const containerRef: { current: HTMLElement } = { current: null } const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false) @@ -43,7 +51,12 @@ export const Panel = (props: Props) => { }) const handleSaveClick = () => { - saveShout(form) + const hasTopics = form.selectedTopics?.length > 0 + if (hasTopics) { + saveShout(form) + } else { + saveDraft(form) + } } const html = useEditorHTML(() => editorRef.current()) diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index 44401b1c..18e8792c 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -36,6 +36,7 @@ import { UploadModalContent } from './UploadModalContent' import { Figcaption } from './extensions/Figcaption' import { Figure } from './extensions/Figure' +import { Loading } from '../_shared/Loading' import styles from './SimplifiedEditor.module.scss' type Props = { @@ -47,17 +48,20 @@ type Props = { onChange?: (text: string) => void variant?: 'minimal' | 'bordered' maxLength?: number + noLimits?: boolean maxHeight?: number submitButtonText?: string quoteEnabled?: boolean imageEnabled?: boolean setClear?: boolean + resetToInitial?: boolean smallHeight?: boolean submitByCtrlEnter?: boolean onlyBubbleControls?: boolean controlsAlwaysVisible?: boolean autoFocus?: boolean isCancelButtonVisible?: boolean + isPosting?: boolean } const DEFAULT_MAX_LENGTH = 400 @@ -122,7 +126,7 @@ const SimplifiedEditor = (props: Props) => { openOnClick: false, }), CharacterCount.configure({ - limit: maxLength, + limit: props.noLimits ? null : maxLength, }), Blockquote.configure({ HTMLAttributes: { @@ -214,6 +218,10 @@ const SimplifiedEditor = (props: Props) => { if (props.setClear) { editor().commands.clearContent(true) } + if (props.resetToInitial) { + editor().commands.clearContent(true) + editor().commands.setContent(props.initialContent) + } }) const handleKeyDown = (event) => { @@ -365,12 +373,14 @@ const SimplifiedEditor = (props: Props) => {
    diff --git a/src/components/Editor/UploadModalContent/UploadModalContent.tsx b/src/components/Editor/UploadModalContent/UploadModalContent.tsx index c2e99ad4..51b0939f 100644 --- a/src/components/Editor/UploadModalContent/UploadModalContent.tsx +++ b/src/components/Editor/UploadModalContent/UploadModalContent.tsx @@ -12,6 +12,7 @@ import { Icon } from '../../_shared/Icon' import { Loading } from '../../_shared/Loading' import { InlineForm } from '../InlineForm' +import { useSession } from '../../../context/session' import styles from './UploadModalContent.module.scss' type Props = { @@ -24,12 +25,12 @@ export const UploadModalContent = (props: Props) => { const [uploadError, setUploadError] = createSignal() const [dragActive, setDragActive] = createSignal(false) const [dragError, setDragError] = createSignal() - + const { session } = useSession() const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' }) const runUpload = async (file: UploadFile) => { try { setIsUploading(true) - const result = await handleImageUpload(file) + const result = await handleImageUpload(file, session()?.access_token) props.onClose(result) setIsUploading(false) } catch (error) { diff --git a/src/components/Feed/ArticleCard/ArticleCard.module.scss b/src/components/Feed/ArticleCard/ArticleCard.module.scss index 336d82c8..d19865b1 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.module.scss +++ b/src/components/Feed/ArticleCard/ArticleCard.module.scss @@ -603,6 +603,7 @@ .shoutCardDetailsItem { align-items: center; display: flex; + font-size: 1.4rem; margin-right: 1.2rem; white-space: nowrap; diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index a22db42d..9fad27b5 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -89,8 +89,8 @@ const getTitleAndSubtitle = ( } const getMainTopicTitle = (article: Shout, lng: string) => { - const mainTopicSlug = article.main_topic || '' - const mainTopic = article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug) + const mainTopicSlug = article?.main_topic || '' + const mainTopic = article?.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug) const mainTopicTitle = mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || '' @@ -111,8 +111,8 @@ export const ArticleCard = (props: ArticleCardProps) => { const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false) const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true) - const description = getDescription(props.article.body) - const aspectRatio = () => LAYOUT_ASPECT[props.article.layout] + const description = getDescription(props.article?.body) + const aspectRatio = () => LAYOUT_ASPECT[props.article?.layout] const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang()) const { title, subtitle } = getTitleAndSubtitle(props.article) @@ -328,7 +328,7 @@ export const ArticleCard = (props: ArticleCardProps) => { {(triggerRef: (el) => void) => (
    - + { const { t } = useLocalize() - const { seen } = useSeenStore() + const { seen } = useSeen() const { subscriptions } = useFollowing() const { page } = useRouter() - const { articlesByTopic } = useArticlesStore() + const { articlesByTopic, articlesByAuthor } = useArticlesStore() const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true) const checkTopicIsSeen = (topicSlug: string) => { @@ -26,8 +25,9 @@ export const Sidebar = () => { } const checkAuthorIsSeen = (authorSlug: string) => { - return Boolean(seen()[authorSlug]) + return articlesByAuthor()[authorSlug]?.every((article) => Boolean(seen()[article.slug])) } + return (
      diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 6b4b8d3f..c7a26d68 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -31,7 +31,7 @@ export const LoginForm = () => { const [isSubmitting, setIsSubmitting] = createSignal(false) const [password, setPassword] = createSignal('') const [validationErrors, setValidationErrors] = createSignal({}) - + // FIXME: use signal or remove const [_isLinkSent, setIsLinkSent] = createSignal(false) const authFormRef: { current: HTMLFormElement } = { current: null } const { showSnackbar } = useSnackbar() @@ -97,7 +97,12 @@ export const LoginForm = () => { const { errors } = await signIn({ email: email(), password: password() }) console.error('[signIn errors]', errors) if (errors?.length > 0) { - if (errors.some((error) => error.message.includes('bad user credentials'))) { + if ( + errors.some( + (error) => + error.message.includes('bad user credentials') || error.message.includes('user not found'), + ) + ) { setValidationErrors((prev) => ({ ...prev, password: t('Something went wrong, check email and password'), diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx index a478225d..a2ec9185 100644 --- a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx @@ -16,6 +16,9 @@ type Props = { onBlur?: (value: string) => void variant?: 'login' | 'registration' disableAutocomplete?: boolean + noValidate?: boolean + onFocus?: () => void + value?: string } const minLength = 8 @@ -27,7 +30,7 @@ export const PasswordField = (props: Props) => { const [showPassword, setShowPassword] = createSignal(false) const [error, setError] = createSignal() - const validatePassword = (passwordToCheck) => { + const validatePassword = (passwordToCheck: string) => { if (passwordToCheck.length < minLength) { return t('Password should be at least 8 characters') } @@ -50,11 +53,13 @@ export const PasswordField = (props: Props) => { } props.onInput(value) - const errorValue = validatePassword(value) - if (errorValue) { - setError(errorValue) - } else { - setError() + if (!props.noValidate) { + const errorValue = validatePassword(value) + if (errorValue) { + setError(errorValue) + } else { + setError() + } } } @@ -78,6 +83,8 @@ export const PasswordField = (props: Props) => { id="password" name="password" disabled={props.disabled} + onFocus={props.onFocus} + value={props.value ? props.value : ''} autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'} type={showPassword() ? 'text' : 'password'} placeholder={props.placeholder || t('Password')} diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index 58ac511a..c58459aa 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -32,6 +32,7 @@ export const RegisterForm = () => { const { changeSearchParams } = useRouter() const { t } = useLocalize() const { signUp, isRegistered, resendVerifyEmail } = useSession() + // FIXME: use submit error data or remove signal const [_submitError, setSubmitError] = createSignal('') const [fullName, setFullName] = createSignal('') const [password, setPassword] = createSignal('') diff --git a/src/components/Nav/AuthModal/SendResetLinkForm.tsx b/src/components/Nav/AuthModal/SendResetLinkForm.tsx index e872cbeb..57f55b6d 100644 --- a/src/components/Nav/AuthModal/SendResetLinkForm.tsx +++ b/src/components/Nav/AuthModal/SendResetLinkForm.tsx @@ -61,7 +61,12 @@ export const SendResetLinkForm = () => { redirect_uri: window.location.origin, }) console.debug('[SendResetLinkForm] authorizer response:', data) - if (errors?.some((error) => error.message.includes('bad user credentials'))) { + if ( + errors?.some( + (error) => + error.message.includes('bad user credentials') || error.message.includes('user not found'), + ) + ) { setIsUserNotFound(true) } if (data.message) setMessage(data.message) diff --git a/src/components/Nav/Header/Header.tsx b/src/components/Nav/Header/Header.tsx index 312a6e1f..680dd886 100644 --- a/src/components/Nav/Header/Header.tsx +++ b/src/components/Nav/Header/Header.tsx @@ -48,7 +48,7 @@ export const Header = (props: Props) => { const { page } = useRouter() const { requireAuthentication } = useSession() const { searchParams } = useRouter() - const { topics } = useTopics() + const { sortedTopics: topics } = useTopics() const [randomTopics, setRandomTopics] = createSignal([]) const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false) const [getIsScrolled, setIsScrolled] = createSignal(false) @@ -59,7 +59,7 @@ export const Header = (props: Props) => { const [isTopicsVisible, setIsTopicsVisible] = createSignal(false) const [isZineVisible, setIsZineVisible] = createSignal(false) const [isFeedVisible, setIsFeedVisible] = createSignal(false) - const { isAuthenticated } = useSession() + const { session } = useSession() const toggleFixed = () => setFixed(!fixed()) @@ -69,7 +69,9 @@ export const Header = (props: Props) => { let windowScrollTop = 0 createEffect(() => { - setRandomTopics(getRandomTopicsFromArray(topics())) + if (topics()?.length) { + setRandomTopics(getRandomTopicsFromArray(topics())) + } }) createEffect(() => { @@ -333,7 +335,7 @@ export const Header = (props: Props) => {
      { const { t } = useLocalize() const { page } = useRouter() - const { session, author, isAuthenticated, isSessionLoaded } = useSession() + const { session, author, isSessionLoaded } = useSession() const { unreadNotificationsCount, showNotificationsPanel } = useNotifications() - const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext() + const { form, toggleEditorPanel, publishShout } = useEditorContext() const handleBellIconClick = (event: Event) => { event.preventDefault() - if (!isAuthenticated()) { + if (!session()?.access_token) { showModal('auth') return } @@ -48,21 +48,17 @@ export const HeaderAuth = (props: Props) => { } const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings') - const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage()) - const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage()) + const isNotificationsVisible = createMemo(() => session()?.access_token && !isEditorPage()) + const isSaveButtonVisible = createMemo(() => session()?.access_token && isEditorPage()) const isCreatePostButtonVisible = createMemo(() => !isEditorPage()) const isAuthenticatedControlsVisible = createMemo( - () => isAuthenticated() && session()?.user?.email_verified, + () => session()?.access_token && session()?.user?.email_verified, ) const handleBurgerButtonClick = () => { toggleEditorPanel() } - const _handleSaveButtonClick = () => { - saveShout(form) - } - const [width, setWidth] = createSignal(0) const [editorMode, setEditorMode] = createSignal(t('Editing')) @@ -106,14 +102,8 @@ export const HeaderAuth = (props: Props) => {
      - -
      + + - -
      + + @@ -239,12 +222,12 @@ export const HeaderAuth = (props: Props) => { + @@ -268,7 +251,7 @@ export const HeaderAuth = (props: Props) => {
      - + { props.setIsProfilePopupVisible(isVisible) diff --git a/src/components/Nav/ProfilePopup.tsx b/src/components/Nav/ProfilePopup.tsx index ce645d6d..f29509cc 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -5,8 +5,10 @@ import { getPagePath } from '@nanostores/router' import { useLocalize } from '../../context/localize' import { useSession } from '../../context/session' import { router } from '../../stores/router' +import { Icon } from '../_shared/Icon' import { Popup } from '../_shared/Popup' +import { clsx } from 'clsx' import styles from '../_shared/Popup/Popup.module.scss' type ProfilePopupProps = Omit @@ -16,30 +18,53 @@ export const ProfilePopup = (props: ProfilePopupProps) => { const { t } = useLocalize() return ( - +
      • - {t('Profile')} + + + {t('Profile')} +
      • - {t('Drafts')} + + + {t('Drafts')} +
      • - + + {t('Subscriptions')}
      • - {t('Comments')} + + + {t('Comments')} +
      • - {t('Bookmarks')} + + + {t('Bookmarks')} +
      • - {t('Settings')} + + + {t('Settings')} +
      • - signOut()}> + signOut()}> + {t('Logout')}
      • diff --git a/src/components/Nav/Snackbar.module.scss b/src/components/Nav/Snackbar.module.scss index 9af5719b..49a7337d 100644 --- a/src/components/Nav/Snackbar.module.scss +++ b/src/components/Nav/Snackbar.module.scss @@ -3,7 +3,10 @@ color: #fff; font-size: 2rem; font-weight: 500; + left: 0; transition: background-color 0.3s; + position: absolute; + width: 100%; &.error { background-color: #d00820; diff --git a/src/components/NotificationsPanel/NotificationsPanel.tsx b/src/components/NotificationsPanel/NotificationsPanel.tsx index 8d98292a..45c4cd3f 100644 --- a/src/components/NotificationsPanel/NotificationsPanel.tsx +++ b/src/components/NotificationsPanel/NotificationsPanel.tsx @@ -46,7 +46,7 @@ const isEarlier = (date: Date) => { export const NotificationsPanel = (props: Props) => { const [isLoading, setIsLoading] = createSignal(false) - const { isAuthenticated } = useSession() + const { author } = useSession() const { t } = useLocalize() const { after, @@ -150,16 +150,13 @@ export const NotificationsPanel = (props: Props) => { }) createEffect( - on( - () => isAuthenticated(), - async () => { - if (isAuthenticated()) { - setIsLoading(true) - await loadNextPage() - setIsLoading(false) - } - }, - ), + on(author, async (a) => { + if (a?.id) { + setIsLoading(true) + await loadNextPage() + setIsLoading(false) + } + }), ) return ( diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index 9d405a9b..a74597a5 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -20,6 +20,8 @@ import { useLocalize } from '../../context/localize' import { useProfileForm } from '../../context/profile' import { useSession } from '../../context/session' import { useSnackbar } from '../../context/snackbar' +import { ProfileInput } from '../../graphql/schema/core.gen' +import styles from '../../pages/profile/Settings.module.scss' import { hideModal, showModal } from '../../stores/ui' import { clone } from '../../utils/clone' import { getImageUrl } from '../../utils/getImageUrl' @@ -35,14 +37,12 @@ import { Loading } from '../_shared/Loading' import { Popover } from '../_shared/Popover' import { SocialNetworkInput } from '../_shared/SocialNetworkInput' -import styles from '../../pages/profile/Settings.module.scss' - const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor')) const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTextarea/GrowingTextarea')) export const ProfileSettings = () => { const { t } = useLocalize() - const [prevForm, setPrevForm] = createStore({}) + const [prevForm, setPrevForm] = createStore({}) const [isFormInitialized, setIsFormInitialized] = createSignal(false) const [isSaving, setIsSaving] = createSignal(false) const [social, setSocial] = createSignal([]) @@ -57,8 +57,9 @@ export const ProfileSettings = () => { const [nameError, setNameError] = createSignal() const { form, submit, updateFormField, setForm } = useProfileForm() const { showSnackbar } = useSnackbar() - const { loadAuthor } = useSession() + const { loadAuthor, session } = useSession() const { showConfirm } = useConfirm() + const [clearAbout, setClearAbout] = createSignal(false) createEffect(() => { if (Object.keys(form).length > 0 && !isFormInitialized()) { @@ -121,7 +122,9 @@ export const ProfileSettings = () => { declineButtonVariant: 'secondary', }) if (isConfirmed) { + setClearAbout(true) setForm(clone(prevForm)) + setClearAbout(false) } } @@ -140,7 +143,7 @@ export const ProfileSettings = () => { setUploadError(false) setIsUserpicUpdating(true) - const result = await handleImageUpload(uploadFile) + const result = await handleImageUpload(uploadFile, session()?.access_token) updateFormField('pic', result.url) setUserpicFile(null) @@ -171,11 +174,13 @@ export const ProfileSettings = () => { on( () => deepEqual(form, prevForm), () => { - setIsFloatingPanelVisible(!deepEqual(form, prevForm)) + if (Object.keys(prevForm).length > 0) { + setIsFloatingPanelVisible(!deepEqual(form, prevForm)) + } }, - { defer: true }, ), ) + const handleDeleteSocialLink = (link) => { updateFormField('links', link, true) } @@ -317,6 +322,8 @@ export const ProfileSettings = () => {

        {t('About')}

        { const handleFollowClick = () => { requireAuthentication(() => { - if (isSubscribed()) { - unfollow(FollowingEntity.Topic, props.topic.slug) - } else { - follow(FollowingEntity.Topic, props.topic.slug) - } + isSubscribed() + ? unfollow(FollowingEntity.Topic, props.topic.slug) + : follow(FollowingEntity.Topic, props.topic.slug) }, 'subscribe') } diff --git a/src/components/Topic/TopicBadge/TopicBadge.tsx b/src/components/Topic/TopicBadge/TopicBadge.tsx index 73afe366..fa7e577b 100644 --- a/src/components/Topic/TopicBadge/TopicBadge.tsx +++ b/src/components/Topic/TopicBadge/TopicBadge.tsx @@ -48,7 +48,7 @@ export const TopicBadge = (props: Props) => { lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title return ( -
        +
        diff --git a/src/components/Views/AllAuthors/AllAuthors.tsx b/src/components/Views/AllAuthors/AllAuthors.tsx index 5464891d..38d1742a 100644 --- a/src/components/Views/AllAuthors/AllAuthors.tsx +++ b/src/components/Views/AllAuthors/AllAuthors.tsx @@ -1,8 +1,8 @@ import type { Author } from '../../../graphql/schema/core.gen' -import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' import { For, Show, createMemo, createSignal } from 'solid-js' +import { Meta } from '../../../context/meta' import { useLocalize } from '../../../context/localize' import { useRouter } from '../../../stores/router' diff --git a/src/components/Views/AllTopics/AllTopics.tsx b/src/components/Views/AllTopics/AllTopics.tsx index 92e57527..26dfe848 100644 --- a/src/components/Views/AllTopics/AllTopics.tsx +++ b/src/components/Views/AllTopics/AllTopics.tsx @@ -1,11 +1,11 @@ import type { Topic } from '../../../graphql/schema/core.gen' -import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal } from 'solid-js' import { useLocalize } from '../../../context/localize' +import { Meta } from '../../../context/meta' +import { useTopics } from '../../../context/topics' import { useRouter } from '../../../stores/router' -import { setTopicsSort, useTopicsStore } from '../../../stores/zine/topics' import { capitalize } from '../../../utils/capitalize' import { dummyFilter } from '../../../utils/dummyFilter' import { getImageUrl } from '../../../utils/getImageUrl' @@ -33,11 +33,7 @@ export const AllTopics = (props: Props) => { const [limit, setLimit] = createSignal(PAGE_SIZE) const ALPHABET = lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ#'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'] - - const { sortedTopics } = useTopicsStore({ - topics: props.topics, - sortBy: searchParams().by || 'shouts', - }) + const { sortedTopics, setTopicsSort } = useTopics() createEffect(() => { if (!searchParams().by) { diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index a523f6e4..9c0423ba 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -1,12 +1,12 @@ import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen' import { getPagePath } from '@nanostores/router' -import { Meta, Title } from '@solidjs/meta' import { clsx } from 'clsx' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' import { useFollowing } from '../../../context/following' import { useLocalize } from '../../../context/localize' +import { Meta, Title } from '../../../context/meta' import { useSession } from '../../../context/session' import { apiClient } from '../../../graphql/client/core' import { router, useRouter } from '../../../stores/router' @@ -42,7 +42,6 @@ export const AuthorView = (props: Props) => { const { followers: myFollowers } = useFollowing() const { session } = useSession() const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) - // const { authorEntities } = useAuthorsStore({ authors: [props.author] }) const { page: getPage, searchParams } = useRouter() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false) @@ -244,7 +243,7 @@ export const AuthorView = (props: Props) => { class={styles.longBio} classList={{ [styles.longBioExpanded]: isBioExpanded() }} > -
        (bioContainerRef.current = el)} innerHTML={author().about} /> +
        (bioContainerRef.current = el)} innerHTML={author()?.about || ''} />
        diff --git a/src/components/Views/DraftsView/DraftsView.tsx b/src/components/Views/DraftsView/DraftsView.tsx index 193b4bc4..957f7ffe 100644 --- a/src/components/Views/DraftsView/DraftsView.tsx +++ b/src/components/Views/DraftsView/DraftsView.tsx @@ -1,6 +1,6 @@ import { openPage } from '@nanostores/router' import { clsx } from 'clsx' -import { For, Show, createEffect, createSignal } from 'solid-js' +import { For, Show, createEffect, createSignal, on } from 'solid-js' import { useEditorContext } from '../../../context/editor' import { useSession } from '../../../context/session' @@ -9,22 +9,28 @@ import { Shout } from '../../../graphql/schema/core.gen' import { router } from '../../../stores/router' import { Draft } from '../../Draft' +import { Loading } from '../../_shared/Loading' import styles from './DraftsView.module.scss' export const DraftsView = () => { - const { isAuthenticated, isSessionLoaded } = useSession() + const { author, loadSession } = useSession() const [drafts, setDrafts] = createSignal([]) - const loadDrafts = async () => { - if (apiClient.private) { - const loadedDrafts = await apiClient.getDrafts() - setDrafts(loadedDrafts.reverse() || []) - } - } - - createEffect(() => { - if (isSessionLoaded()) loadDrafts() - }) + createEffect( + on( + () => author(), + async (a) => { + if (a) { + const { shouts: loadedDrafts, error } = await apiClient.getDrafts() + if (error) { + console.warn(error) + await loadSession() + } + setDrafts(loadedDrafts || []) + } + }, + ), + ) const { publishShoutById, deleteShout } = useEditorContext() @@ -44,22 +50,20 @@ export const DraftsView = () => { return (
        - + }>
        - - - {(draft) => ( - - )} - - + + {(draft) => ( + + )} +
        diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 83da483d..1e8650b1 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -2,6 +2,7 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' +import { throttle } from 'throttle-debounce' import { ShoutForm, useEditorContext } from '../../../context/editor' import { useLocalize } from '../../../context/localize' @@ -41,7 +42,9 @@ export const EMPTY_TOPIC: Topic = { slug: '', } +const THROTTLING_INTERVAL = 2000 const AUTO_SAVE_INTERVAL = 5000 +const AUTO_SAVE_DELAY = 5000 const handleScrollTopButtonClick = (e) => { e.preventDefault() window.scrollTo({ @@ -65,12 +68,14 @@ export const EditView = (props: Props) => { } = useEditorContext() const shoutTopics = props.shout.topics || [] - // TODO: проверить сохранение черновика в local storage (не работает) const draft = getDraftFromLocalStorage(props.shout.id) + if (draft) { - setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }) + const draftForm = Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id } + setForm(draftForm) + console.debug('draft from localstorage: ', draftForm) } else { - setForm({ + const draftForm = { slug: props.shout.slug, shoutId: props.shout.id, title: props.shout.title, @@ -83,7 +88,9 @@ export const EditView = (props: Props) => { coverImageUrl: props.shout.cover, media: props.shout.media, layout: props.shout.layout, - }) + } + setForm(draftForm) + console.debug('draft from props data: ', draftForm) } const subtitleInput: { current: HTMLTextAreaElement } = { current: null } @@ -106,9 +113,6 @@ export const EditView = (props: Props) => { onCleanup(() => { window.removeEventListener('scroll', handleScroll) }) - }) - - onMount(() => { // eslint-disable-next-line unicorn/consistent-function-scoping const handleBeforeUnload = (event) => { if (!deepEqual(prevForm, form)) { @@ -180,42 +184,39 @@ export const EditView = (props: Props) => { let autoSaveTimeOutId: number | string | NodeJS.Timeout - //TODO: add throttle + const autoSave = async () => { + const hasChanges = !deepEqual(form, prevForm) + const hasTopic = Boolean(form.mainTopic) + if (hasChanges || hasTopic) { + console.debug('saving draft', form) + setSaving(true) + saveDraftToLocalStorage(form) + await saveDraft(form) + setPrevForm(clone(form)) + setTimeout(() => setSaving(false), AUTO_SAVE_DELAY) + } + } + + // Throttle the autoSave function + const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave) + const autoSaveRecursive = () => { - autoSaveTimeOutId = setTimeout(async () => { - const hasChanges = !deepEqual(form, prevForm) - if (hasChanges) { - setSaving(true) - if (props.shout?.published_at) { - saveDraftToLocalStorage(form) - } else { - await saveDraft(form) - } - setPrevForm(clone(form)) - setTimeout(() => { - setSaving(false) - }, 2000) - } + autoSaveTimeOutId = setTimeout(() => { + throttledAutoSave() autoSaveRecursive() }, AUTO_SAVE_INTERVAL) } - const stopAutoSave = () => { - clearTimeout(autoSaveTimeOutId) - } - onMount(() => { autoSaveRecursive() - }) - - onCleanup(() => { - stopAutoSave() + onCleanup(() => clearTimeout(autoSaveTimeOutId)) }) const showSubtitleInput = () => { setIsSubtitleVisible(true) subtitleInput.current.focus() } + const showLeadInput = () => { setIsLeadVisible(true) } diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 8506bf24..cb9fa754 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -192,7 +192,7 @@ export const Expo = (props: Props) => { )} 0} keyed={true}> - + {(shout) => ( diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index e457cafc..b2fc5dfe 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -1,18 +1,18 @@ import { getPagePath } from '@nanostores/router' -import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' import { useLocalize } from '../../../context/localize' +import { Meta } from '../../../context/meta' import { useReactions } from '../../../context/reactions' import { useSession } from '../../../context/session' +import { useTopics } from '../../../context/topics' import { apiClient } from '../../../graphql/client/core' import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../../graphql/schema/core.gen' import { router, useRouter } from '../../../stores/router' import { showModal } from '../../../stores/ui' import { resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles' import { useTopAuthorsStore } from '../../../stores/zine/topAuthors' -import { useTopicsStore } from '../../../stores/zine/topics' import { getImageUrl } from '../../../utils/getImageUrl' import { byCreated } from '../../../utils/sortby' import { CommentDate } from '../../Article/CommentDate' @@ -49,7 +49,7 @@ type VisibilityItem = { } type FeedSearchParams = { - by: 'publish_date' | 'likes' | 'comments' + by: 'publish_date' | 'likes' | 'last_comment' period: FeedPeriod visibility: VisibilityMode } @@ -103,7 +103,7 @@ export const FeedView = (props: Props) => { const { session } = useSession() const { loadReactionsBy } = useReactions() const { sortedArticles } = useArticlesStore() - const { topTopics } = useTopicsStore() + const { topTopics } = useTopics() const { topAuthors } = useTopAuthorsStore() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [topComments, setTopComments] = createSignal([]) @@ -258,10 +258,10 @@ export const FeedView = (props: Props) => {
      • - changeSearchParams({ by: 'comments' })}> + changeSearchParams({ by: 'last_comment' })}> {t('Most commented')}
      • diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index b4c321ba..ac22c97b 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -2,6 +2,7 @@ import { getPagePath } from '@nanostores/router' import { For, Show, createMemo, createSignal, onMount } from 'solid-js' import { useLocalize } from '../../context/localize' +import { useTopics } from '../../context/topics' import { Shout, Topic } from '../../graphql/schema/core.gen' import { router } from '../../stores/router' import { @@ -11,7 +12,6 @@ import { useArticlesStore, } from '../../stores/zine/articles' import { useTopAuthorsStore } from '../../stores/zine/topAuthors' -import { useTopicsStore } from '../../stores/zine/topics' import { capitalize } from '../../utils/capitalize' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { splitToPages } from '../../utils/splitToPages' @@ -46,7 +46,7 @@ export const HomeView = (props: Props) => { shouts: props.shouts, }) - const { topTopics } = useTopicsStore() + const { topTopics } = useTopics() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const { topAuthors } = useTopAuthorsStore() const { t } = useLocalize() @@ -110,7 +110,7 @@ export const HomeView = (props: Props) => { nodate={true} /> - + diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 1e9f4a81..44289502 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -6,11 +6,11 @@ import { createStore } from 'solid-js/store' import { ShoutForm, useEditorContext } from '../../../context/editor' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' +import { useTopics } from '../../../context/topics' import { Topic } from '../../../graphql/schema/core.gen' import { UploadedFile } from '../../../pages/types' import { router } from '../../../stores/router' import { hideModal, showModal } from '../../../stores/ui' -import { loadAllTopics, useTopicsStore } from '../../../stores/zine/topics' import { TopicSelect, UploadModalContent } from '../../Editor' import { Modal } from '../../Nav/Modal' import { Button } from '../../_shared/Button' @@ -53,13 +53,13 @@ const emptyConfig = { export const PublishSettings = (props: Props) => { const { t } = useLocalize() const { author } = useSession() - const { sortedTopics } = useTopicsStore() + const { sortedTopics } = useTopics() const { showSnackbar } = useSnackbar() const [topics, setTopics] = createSignal(sortedTopics()) const composeDescription = () => { if (!props.form.description) { - const cleanFootnotes = props.form.body.replaceAll(/.*?<\/footnote>/g, '') + const cleanFootnotes = props.form.body.replaceAll(/(.*?)<\/footnote>/g, '') const leadText = cleanFootnotes.replaceAll(/<\/?[^>]+(>|$)/gi, ' ') return shorten(leadText, DESCRIPTION_MAX_LENGTH).trim() } @@ -82,7 +82,6 @@ export const PublishSettings = (props: Props) => { onMount(() => { setSettingsForm(initialData()) - loadAllTopics() }) createEffect(() => setTopics(sortedTopics())) diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index 97a107dd..d788d1f6 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -1,14 +1,14 @@ import { LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen' -import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' +import { Meta } from '../../context/meta' import { useLocalize } from '../../context/localize' +import { useTopics } from '../../context/topics' import { useRouter } from '../../stores/router' import { loadShouts, useArticlesStore } from '../../stores/zine/articles' import { useAuthorsStore } from '../../stores/zine/authors' -import { useTopicsStore } from '../../stores/zine/topics' import { capitalize } from '../../utils/capitalize' import { getImageUrl } from '../../utils/getImageUrl' import { getDescription } from '../../utils/meta' @@ -43,7 +43,7 @@ export const TopicView = (props: Props) => { const { searchParams, changeSearchParams } = useRouter() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) - const { topicEntities } = useTopicsStore({ topics: [props.topic] }) + const { topicEntities } = useTopics() const { authorsByTopic } = useAuthorsStore() const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) @@ -216,7 +216,7 @@ export const TopicView = (props: Props) => { wrapper={'author'} /> 0} keyed={true}> - + { const [dragActive, setDragActive] = createSignal(false) const [dropAreaError, setDropAreaError] = createSignal() const [loading, setLoading] = createSignal(false) + const { session } = useSession() const runUpload = async (files) => { try { @@ -35,7 +37,7 @@ export const DropArea = (props: Props) => { const results: UploadedFile[] = [] for (const file of files) { const handler = props.fileType === 'image' ? handleImageUpload : handleFileUpload - const result = await handler(file) + const result = await handler(file, session()?.access_token) results.push(result) } props.onUpload(results) diff --git a/src/components/_shared/DropDown/DropDown.tsx b/src/components/_shared/DropDown/DropDown.tsx index 48115e5b..e32e7413 100644 --- a/src/components/_shared/DropDown/DropDown.tsx +++ b/src/components/_shared/DropDown/DropDown.tsx @@ -5,6 +5,7 @@ import { For, Show, createSignal } from 'solid-js' import { Popup } from '../Popup' +import popupStyles from '../Popup/Popup.module.scss' import styles from './DropDown.module.scss' export type Option = { @@ -56,16 +57,22 @@ export const DropDown = (props: Props) onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)} {...props.popupProps} > - - {(option) => ( -
        props.onChange(option)} - > - {option.title} -
        - )} -
        +
          + + {(option) => ( +
        • + +
        • + )} +
          +
        ) diff --git a/src/components/_shared/Image/Image.tsx b/src/components/_shared/Image/Image.tsx index 413f66c8..bfb5a6d3 100644 --- a/src/components/_shared/Image/Image.tsx +++ b/src/components/_shared/Image/Image.tsx @@ -1,7 +1,7 @@ import type { JSX } from 'solid-js' -import { Link } from '@solidjs/meta' import { splitProps } from 'solid-js' +import { Link } from '../../../context/meta' import { getImageUrl } from '../../../utils/getImageUrl' diff --git a/src/components/_shared/PageLayout.tsx b/src/components/_shared/PageLayout.tsx index 1cd7abc9..de35813e 100644 --- a/src/components/_shared/PageLayout.tsx +++ b/src/components/_shared/PageLayout.tsx @@ -1,8 +1,8 @@ import type { JSX } from 'solid-js' -import { Title } from '@solidjs/meta' import { clsx } from 'clsx' import { Show, createEffect, createSignal } from 'solid-js' +import { Title } from '../../context/meta' import { Footer } from '../Discours/Footer' import { Header } from '../Nav/Header' diff --git a/src/components/_shared/Popup/Popup.module.scss b/src/components/_shared/Popup/Popup.module.scss index 5db976f9..3ae6d95c 100644 --- a/src/components/_shared/Popup/Popup.module.scss +++ b/src/components/_shared/Popup/Popup.module.scss @@ -8,10 +8,14 @@ .popup { background: var(--background-color); + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 1.6rem; + box-shadow: 0 8px 16px 0 rgb(0 0 0 / 5%); color: var(--default-color); cursor: default; min-width: 144px; opacity: 1; + overflow: hidden; position: absolute; text-align: left; top: calc(100% + 11px); @@ -21,6 +25,7 @@ margin-bottom: 0; li { + margin: 0; position: relative; &:last-child { @@ -29,33 +34,19 @@ } } - &.bordered { - @include font-size(1.6rem); - - border: 2px solid #000; - padding: 2.4rem; - - ul li { - margin-bottom: 1.6rem; - - &:last-child { - margin-bottom: 0; - } - } - } - &.tiny { @include font-size(1.4rem); - box-shadow: 0 4px 60px rgb(0 0 0 / 10%); - padding: 1rem; + .action { + padding: 0.5rem 1rem; + } - ul li { - margin-bottom: 1rem; + li:first-child .action { + padding-top: 1rem; + } - &:last-child { - margin-bottom: 0; - } + li:last-child .action { + padding-bottom: 1rem; } } @@ -75,7 +66,6 @@ .topBorderItem { border-top: 2px solid; - padding-top: 1em; } a:link, @@ -83,26 +73,64 @@ border: none; white-space: nowrap; - &:hover { - .icon img { - filter: invert(0); - } + &::before { + content: ''; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; } } .icon { display: inline-block; - width: 3.6rem; + margin-right: 1rem; + width: 2.4rem; img { display: inline-block; - filter: invert(1); - max-height: 2rem; - max-width: 2rem; + max-height: 2.4rem; + max-width: 2.4rem; transition: filter 0.3s; vertical-align: middle; } } + + .action { + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; + padding: 8px 16px; + font-size: inherit; + font-weight: 500; + text-align: left; + white-space: nowrap; + + &:hover { + background: var(--black-500); + color: var(--black-50) !important; + + .icon img { + filter: invert(1); + } + } + } + + li:first-child .action { + padding-top: 16px; + } + + li:last-child .action { + padding-bottom: 16px; + } +} + +.profilePopup { + @include media-breakpoint-up(sm) { + min-width: 22rem; + } } // TODO: animation diff --git a/src/components/_shared/Popup/Popup.tsx b/src/components/_shared/Popup/Popup.tsx index e9e2a49c..1d9fb7b2 100644 --- a/src/components/_shared/Popup/Popup.tsx +++ b/src/components/_shared/Popup/Popup.tsx @@ -14,7 +14,7 @@ export type PopupProps = { children: JSX.Element onVisibilityChange?: (isVisible: boolean) => void horizontalAnchor?: HorizontalAnchor - variant?: 'bordered' | 'tiny' + variant?: 'tiny' closePopup?: boolean } @@ -54,7 +54,6 @@ export const Popup = (props: PopupProps) => { class={clsx(styles.popup, props.popupCssClass, { [styles.horizontalAnchorCenter]: horizontalAnchor === 'center', [styles.horizontalAnchorRight]: horizontalAnchor === 'right', - [styles.bordered]: props.variant === 'bordered', [styles.tiny]: props.variant === 'tiny', })} > diff --git a/src/components/_shared/ShareLinks/ShareLinks.module.scss b/src/components/_shared/ShareLinks/ShareLinks.module.scss index d65ee4aa..0066714d 100644 --- a/src/components/_shared/ShareLinks/ShareLinks.module.scss +++ b/src/components/_shared/ShareLinks/ShareLinks.module.scss @@ -7,9 +7,6 @@ white-space: nowrap; &:hover { - background: #000; - color: #fff; - .icon img { filter: invert(0); } @@ -17,7 +14,7 @@ .icon { display: inline-block; - width: 3.6rem; + width: 2rem; img { display: inline-block; diff --git a/src/components/_shared/ShareLinks/ShareLinks.tsx b/src/components/_shared/ShareLinks/ShareLinks.tsx index 0e1bd98f..fd7fd8bc 100644 --- a/src/components/_shared/ShareLinks/ShareLinks.tsx +++ b/src/components/_shared/ShareLinks/ShareLinks.tsx @@ -7,6 +7,7 @@ import { useSnackbar } from '../../../context/snackbar' import { Icon } from '../Icon' import { Popover } from '../Popover' +import popupStyles from '../Popup/Popup.module.scss' import styles from './ShareLinks.module.scss' type Props = { @@ -53,26 +54,42 @@ export const ShareLinks = (props: Props) => {
        • -
        • -
        • -
        • -
        • @@ -80,8 +97,12 @@ export const ShareLinks = (props: Props) => { - + } @@ -93,7 +114,7 @@ export const ShareLinks = (props: Props) => { {(triggerRef: (el) => void) => (
          - +
          )}
          diff --git a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx index e8f76d6a..c614cba1 100644 --- a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx +++ b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx @@ -10,6 +10,8 @@ import { ShowOnlyOnClient } from '../ShowOnlyOnClient' import { SwiperRef } from './swiper' +import { Row1 } from '../../Feed/Row1' +import { Row2 } from '../../Feed/Row2' import styles from './Swiper.module.scss' type Props = { @@ -21,70 +23,82 @@ export const ArticleCardSwiper = (props: Props) => { const mainSwipeRef: { current: SwiperRef } = { current: null } onMount(async () => { - const { register } = await import('swiper/element/bundle') - register() - SwiperCore.use([Pagination, Navigation, Manipulation]) + if (props.slides.length > 1) { + const { register } = await import('swiper/element/bundle') + register() + SwiperCore.use([Pagination, Navigation, Manipulation]) + } }) return ( -
          +
          1, + [styles.articleMode]: true, + [styles.ArticleCardSwiper]: props.slides.length > 1, + })} + >

          {props.title}

          0}> -
          - (mainSwipeRef.current = el)} - centered-slides={true} - observer={true} - space-between={10} - breakpoints={{ - 576: { spaceBetween: 20, slidesPerView: 1.5 }, - 992: { spaceBetween: 52, slidesPerView: 1.5 }, - }} - round-lengths={true} - loop={true} - speed={800} - autoplay={{ - disableOnInteraction: false, - delay: 6000, - pauseOnMouseEnter: true, - }} - > - - {(slide, index) => ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - - - - )} - - -
          mainSwipeRef.current.swiper.slidePrev()} - > - -
          -
          mainSwipeRef.current.swiper.slideNext()} - > - -
          -
          + }> + }> +
          + (mainSwipeRef.current = el)} + centered-slides={true} + observer={true} + space-between={10} + breakpoints={{ + 576: { spaceBetween: 20, slidesPerView: 1.5 }, + 992: { spaceBetween: 52, slidesPerView: 1.5 }, + }} + round-lengths={true} + loop={true} + speed={800} + autoplay={{ + disableOnInteraction: false, + delay: 6000, + pauseOnMouseEnter: true, + }} + > + + {(slide, index) => ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + + + + )} + + +
          mainSwipeRef.current.swiper.slidePrev()} + > + +
          +
          mainSwipeRef.current.swiper.slideNext()} + > + +
          +
          +
          +
          diff --git a/src/components/_shared/SolidSwiper/EditorSwiper.tsx b/src/components/_shared/SolidSwiper/EditorSwiper.tsx index c7f53a03..83d691a8 100644 --- a/src/components/_shared/SolidSwiper/EditorSwiper.tsx +++ b/src/components/_shared/SolidSwiper/EditorSwiper.tsx @@ -19,6 +19,7 @@ import { Popover } from '../Popover' import { SwiperRef } from './swiper' +import { useSession } from '../../../context/session' import styles from './Swiper.module.scss' const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) @@ -36,7 +37,7 @@ export const EditorSwiper = (props: Props) => { const [loading, setLoading] = createSignal(false) const [slideIndex, setSlideIndex] = createSignal(0) const [slideBody, setSlideBody] = createSignal() - + const { session } = useSession() const mainSwipeRef: { current: SwiperRef } = { current: null } const thumbSwipeRef: { current: SwiperRef } = { current: null } @@ -100,7 +101,7 @@ export const EditorSwiper = (props: Props) => { setLoading(true) const results: UploadedFile[] = [] for (const file of selectedFiles) { - const result = await handleImageUpload(file) + const result = await handleImageUpload(file, session()?.access_token) results.push(result) } props.onImagesAdd(composeMediaItems(results)) diff --git a/src/components/_shared/SolidSwiper/Swiper.module.scss b/src/components/_shared/SolidSwiper/Swiper.module.scss index f3f182e8..f66de2fc 100644 --- a/src/components/_shared/SolidSwiper/Swiper.module.scss +++ b/src/components/_shared/SolidSwiper/Swiper.module.scss @@ -57,7 +57,7 @@ margin: 0; position: relative; - & > swiper-container { + &>swiper-container { display: flex; flex-direction: row; gap: 10px; diff --git a/src/context/meta.tsx b/src/context/meta.tsx new file mode 100644 index 00000000..bf2e1a1d --- /dev/null +++ b/src/context/meta.tsx @@ -0,0 +1,275 @@ +import { + Component, + JSX, + ParentComponent, + createContext, + createRenderEffect, + createUniqueId, + onCleanup, + sharedConfig, + useContext, +} from 'solid-js' +import { escape as escapeMeta, isServer, spread, ssr, useAssets } from 'solid-js/web' + +export const MetaContext = createContext() + +interface TagDescription { + tag: string + props: Record + setting?: { close?: boolean; escape?: boolean } + id: string + name?: string + ref?: Element +} + +export interface MetaContextType { + addTag: (tag: TagDescription) => number + removeTag: (tag: TagDescription, index: number) => void +} + +const cascadingTags = ['title', 'meta'] + +// https://html.spec.whatwg.org/multipage/semantics.html#the-title-element +const titleTagProperties: string[] = [] + +const metaTagProperties: string[] = + // https://html.spec.whatwg.org/multipage/semantics.html#the-meta-element + ['name', 'http-equiv', 'content', 'charset', 'media'] + // additional properties + .concat(['property']) + +const getTagKey = (tag: TagDescription, properties: string[]) => { + // pick allowed properties and sort them + const tagProps = Object.fromEntries( + Object.entries(tag.props) + .filter(([k]) => properties.includes(k)) + .sort(), + ) + + // treat `property` as `name` for meta tags + if (Object.hasOwn(tagProps, 'name') || Object.hasOwn(tagProps, 'property')) { + tagProps.name = tagProps.name || tagProps.property + tagProps.property = undefined + } + + // concat tag name and properties as unique key for this tag + return tag.tag + JSON.stringify(tagProps) +} + +function initClientProvider() { + if (!sharedConfig.context) { + const ssrTags = document.head.querySelectorAll('[data-sm]') + // `forEach` on `NodeList` is not supported in Googlebot, so use a workaround + Array.prototype.forEach.call(ssrTags, (ssrTag: Node) => ssrTag.parentNode?.removeChild(ssrTag)) + } + + const cascadedTagInstances = new Map() + // TODO: use one element for all tags of the same type, just swap out + // where the props get applied + function getElement(tag: TagDescription) { + if (tag.ref) { + return tag.ref + } + let el = document.querySelector(`[data-sm="${tag.id}"]`) + if (el) { + if (el.tagName.toLowerCase() !== tag.tag) { + if (el.parentNode) { + // remove the old tag + el.parentNode.removeChild(el) + } + // add the new tag + el = document.createElement(tag.tag) + } + // use the old tag + el.removeAttribute('data-sm') + } else { + // create a new tag + el = document.createElement(tag.tag) + } + return el + } + + return { + addTag(tag: TagDescription) { + if (cascadingTags.indexOf(tag.tag) !== -1) { + const properties = tag.tag === 'title' ? titleTagProperties : metaTagProperties + const tagKey = getTagKey(tag, properties) + + // only cascading tags need to be kept as singletons + if (!cascadedTagInstances.has(tagKey)) { + cascadedTagInstances.set(tagKey, []) + } + + let instances = cascadedTagInstances.get(tagKey) + const index = instances.length + + instances = [...instances, tag] + + // track indices synchronously + cascadedTagInstances.set(tagKey, instances) + + const element = getElement(tag) + tag.ref = element + + spread(element, tag.props) + + let lastVisited = null + for (let i = index - 1; i >= 0; i--) { + if (instances[i] != null) { + lastVisited = instances[i] + break + } + } + + if (element.parentNode !== document.head) { + document.head.appendChild(element) + } + if (lastVisited?.ref?.parentNode) { + document.head?.removeChild(lastVisited.ref) + } + + return index + } + + const element = getElement(tag) + tag.ref = element + + spread(element, tag.props) + + if (element.parentNode !== document.head) { + document.head.appendChild(element) + } + + return -1 + }, + removeTag(tag: TagDescription, index: number) { + const properties = tag.tag === 'title' ? titleTagProperties : metaTagProperties + const tagKey = getTagKey(tag, properties) + + if (tag.ref) { + const t = cascadedTagInstances.get(tagKey) + if (t) { + if (tag.ref.parentNode) { + tag.ref.parentNode.removeChild(tag.ref) + for (let i = index - 1; i >= 0; i--) { + if (t[i] != null) { + document.head.appendChild(t[i].ref) + } + } + } + + t[index] = null + cascadedTagInstances.set(tagKey, t) + } else if (tag.ref.parentNode) { + tag.ref.parentNode.removeChild(tag.ref) + } + } + }, + } +} + +function initServerProvider() { + const tags: TagDescription[] = [] + useAssets(() => { + const rendered = renderTags(tags) + return ssr(rendered as string) as unknown as Element + }) + + return { + addTag(tagDesc: TagDescription) { + // tweak only cascading tags + if (cascadingTags.indexOf(tagDesc.tag) !== -1) { + const properties = tagDesc.tag === 'title' ? titleTagProperties : metaTagProperties + const tagDescKey = getTagKey(tagDesc, properties) + const index = tags.findIndex( + (prev) => prev.tag === tagDesc.tag && getTagKey(prev, properties) === tagDescKey, + ) + if (index !== -1) { + tags.splice(index, 1) + } + } + tags.push(tagDesc) + return tags.length + }, + // biome-ignore lint/suspicious/noEmptyBlockStatements: initial value + removeTag(_tag: TagDescription, _index: number) {}, + } +} + +export const MetaProvider: ParentComponent = (props) => { + const actions = isServer ? initServerProvider() : initClientProvider() + return {props.children} +} + +const MetaTag = ( + tag: string, + props: { [k: string]: string }, + setting?: { escape?: boolean; close?: boolean }, +) => { + useHead({ + tag, + props, + setting, + id: createUniqueId(), + get name() { + return props.name || props.property + }, + }) + + return null +} + +export function useHead(tagDesc: TagDescription) { + const c = useContext(MetaContext) + if (!c) throw new Error(' should be in the tree') + + createRenderEffect(() => { + const index = c?.addTag(tagDesc) + onCleanup(() => c?.removeTag(tagDesc, index)) + }) +} + +function renderTags(tags: TagDescription[]) { + return tags + .map((tag) => { + const keys = Object.keys(tag.props) + const props = keys + .map((k) => + k === 'children' + ? '' + : ` ${k}="${ + // @ts-expect-error + escapeMeta(tag.props[k], true) + }"`, + ) + .join('') + const children = tag.props.children + if (tag.setting?.close) { + return `<${tag.tag} data-sm="${tag.id}"${props}>${ + // @ts-expect-error + tag.setting?.escape ? escapeMeta(children) : children || '' + }` + } + return `<${tag.tag} data-sm="${tag.id}"${props}/>` + }) + .join('') +} + +export const Title: Component> = (props) => + MetaTag('title', props as { [k: string]: string }, { escape: true, close: true }) + +export const Style: Component> = (props) => + MetaTag('style', props as { [k: string]: string }, { close: true }) + +export const Meta: Component> = (props) => + MetaTag('meta', props as { [k: string]: string }) + +export const Link: Component> = (props) => + MetaTag('link', props as { [k: string]: string }) + +export const Base: Component> = (props) => + MetaTag('base', props as { [k: string]: string }) + +export const Stylesheet: Component, 'rel'>> = (props) => ( + +) diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx index b25ca037..7233f426 100644 --- a/src/context/notifications.tsx +++ b/src/context/notifications.tsx @@ -40,11 +40,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0) const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0) const [notificationEntities, setNotificationEntities] = createStore>({}) - const { isAuthenticated } = useSession() + const { author } = useSession() const { addHandler } = useConnect() const loadNotificationsGrouped = async (options: { after: number; limit?: number; offset?: number }) => { - if (isAuthenticated() && notifierClient?.private) { + if (author()?.id && notifierClient?.private) { const notificationsResult = await notifierClient.getNotifications(options) const groups = notificationsResult?.notifications || [] const total = notificationsResult?.total || 0 @@ -74,7 +74,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { onMount(() => { addHandler((data: SSEMessage) => { - if (data.entity === 'reaction' && isAuthenticated()) { + if (data.entity === 'reaction' && author()?.id) { console.info('[context.notifications] event', data) loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) }) } @@ -91,14 +91,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { } const markSeenAll = async () => { - if (isAuthenticated() && notifierClient.private) { + if (author()?.id && notifierClient.private) { await notifierClient.markSeenAfter({ after: after() }) await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() }) } } const markSeen = async (notification_id: number) => { - if (isAuthenticated() && notifierClient.private) { + if (author()?.id && notifierClient.private) { await notifierClient.markSeen(notification_id) await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() }) } diff --git a/src/context/seen.tsx b/src/context/seen.tsx new file mode 100644 index 00000000..bd4a26cd --- /dev/null +++ b/src/context/seen.tsx @@ -0,0 +1,29 @@ +import { makePersisted } from '@solid-primitives/storage' +import { Accessor, JSX, createContext, createSignal, useContext } from 'solid-js' + +type SeenContextType = { + seen: Accessor<{ [slug: string]: number }> + addSeen: (slug: string) => void +} + +const SeenContext = createContext() +export function useSeen() { + return useContext(SeenContext) +} + +export const SeenProvider = (props: { children: JSX.Element }) => { + const [seen, setSeen] = makePersisted(createSignal<{ [slug: string]: number }>({}), { + name: 'discoursio-seen', + }) + + const addSeen = async (slug: string) => { + setSeen((prev) => { + const newSeen = { ...prev, [slug]: Date.now() } + return newSeen + }) + } + + const value: SeenContextType = { seen, addSeen } + + return {props.children} +} diff --git a/src/context/session.tsx b/src/context/session.tsx index 8fbd38e5..27ed21e5 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -13,6 +13,7 @@ import { LoginInput, ResendVerifyEmailInput, SignupInput, + UpdateProfileInput, VerifyEmailInput, } from '@authorizerdev/authorizer-js' import { @@ -48,7 +49,6 @@ export type SessionContextType = { author: Resource authError: Accessor isSessionLoaded: Accessor - isAuthenticated: Accessor loadSession: () => AuthToken | Promise setSession: (token: AuthToken | null) => void // setSession loadAuthor: (info?: unknown) => Author | Promise @@ -59,6 +59,7 @@ export type SessionContextType = { ) => void signUp: (params: SignupInput) => Promise<{ data: AuthToken; errors: Error[] }> signIn: (params: LoginInput) => Promise<{ data: AuthToken; errors: Error[] }> + updateProfile: (params: UpdateProfileInput) => Promise<{ data: AuthToken; errors: Error[] }> signOut: () => Promise oauth: (provider: string) => Promise forgotPassword: ( @@ -224,9 +225,12 @@ export const SessionProvider = (props: { const appdata = session()?.user.app_data if (appdata) { const { profile } = appdata - setAuthor(profile) - addAuthors([profile]) - if (!profile) loadAuthor() + if (profile?.id) { + setAuthor(profile) + addAuthors([profile]) + } else { + setTimeout(loadAuthor, 15) + } } } catch (e) { console.error(e) @@ -271,12 +275,9 @@ export const SessionProvider = (props: { // callback state updater createEffect( - on( - () => props.onStateChangeCallback, - () => { - props.onStateChangeCallback(session()) - }, - ), + on([() => props.onStateChangeCallback, session], ([_, ses]) => { + ses?.user?.id && props.onStateChangeCallback(ses) + }), ) const [authCallback, setAuthCallback] = createSignal<() => void>(noop) @@ -309,6 +310,8 @@ export const SessionProvider = (props: { } const signUp = async (params: SignupInput) => await authenticate(authorizer().signup, params) const signIn = async (params: LoginInput) => await authenticate(authorizer().login, params) + const updateProfile = async (params: UpdateProfileInput) => + await authenticate(authorizer().updateProfile, params) const signOut = async () => { const authResult: ApiResponse = await authorizer().logout() @@ -378,9 +381,6 @@ export const SessionProvider = (props: { console.warn(error) } } - - const isAuthenticated = createMemo(() => Boolean(author())) - const actions = { loadSession, requireAuthentication, @@ -388,6 +388,7 @@ export const SessionProvider = (props: { signIn, signOut, confirmEmail, + updateProfile, setIsSessionLoaded, setSession, setAuthor, @@ -405,7 +406,6 @@ export const SessionProvider = (props: { isSessionLoaded, author, ...actions, - isAuthenticated, resendVerifyEmail, } diff --git a/src/context/topics.tsx b/src/context/topics.tsx index f6c40b1b..eb8c3e93 100644 --- a/src/context/topics.tsx +++ b/src/context/topics.tsx @@ -1,10 +1,19 @@ +import { createLazyMemo } from '@solid-primitives/memo' import { openDB } from 'idb' -import { Accessor, JSX, createContext, createSignal, onMount, useContext } from 'solid-js' +import { Accessor, JSX, createContext, createMemo, createSignal, onMount, useContext } from 'solid-js' import { apiClient } from '../graphql/client/core' import { Topic } from '../graphql/schema/core.gen' +import { useRouter } from '../stores/router' +import { byTopicStatDesc } from '../utils/sortby' type TopicsContextType = { - topics: Accessor + topicEntities: Accessor<{ [topicSlug: string]: Topic }> + sortedTopics: Accessor + randomTopics: Accessor + topTopics: Accessor + setTopicsSort: (sortBy: string) => void + addTopics: (topics: Topic[]) => void + loadTopics: () => Promise } const TopicsContext = createContext() @@ -25,11 +34,13 @@ const setupIndexedDB = async () => { }) } -const getTopicsFromIndexedDB = (db) => { +const getTopicsFromIndexedDB = async (db) => { const tx = db.transaction(STORE_NAME, 'readonly') const store = tx.objectStore(STORE_NAME) - return store.getAll() + const topics = await store.getAll() + return { topics, timestamp: tx.done } } + const saveTopicsToIndexedDB = async (db, topics) => { const tx = db.transaction(STORE_NAME, 'readwrite') const store = tx.objectStore(STORE_NAME) @@ -40,20 +51,93 @@ const saveTopicsToIndexedDB = async (db, topics) => { } export const TopicsProvider = (props: { children: JSX.Element }) => { + const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({}) + const [sortAllBy, setSortAllBy] = createSignal<'shouts' | 'followers' | 'authors' | 'title'>('shouts') const [randomTopics, setRandomTopics] = createSignal([]) + const sortedTopics = createLazyMemo(() => { + const topics = Object.values(topicEntities()) + const { changeSearchParams } = useRouter() + switch (sortAllBy()) { + case 'followers': { + topics.sort(byTopicStatDesc('followers')) + break + } + case 'shouts': { + topics.sort(byTopicStatDesc('shouts')) + break + } + case 'authors': { + topics.sort(byTopicStatDesc('authors')) + break + } + case 'title': { + topics.sort((a, b) => a.title.localeCompare(b.title)) + break + } + default: { + topics.sort(byTopicStatDesc('shouts')) + changeSearchParams({ by: 'shouts' }) + } + } + + return topics + }) + + const topTopics = createMemo(() => { + const topics = Object.values(topicEntities()) + topics.sort(byTopicStatDesc('shouts')) + return topics + }) + + const addTopics = (...args: Topic[][]) => { + const allTopics = args.flatMap((topics) => (topics || []).filter(Boolean)) + + const newTopicEntities = allTopics.reduce( + (acc, topic) => { + acc[topic.slug] = topic + return acc + }, + {} as Record, + ) + + setTopicEntities((prevTopicEntities) => { + return { + ...prevTopicEntities, + ...newTopicEntities, + } + }) + } + const [db, setDb] = createSignal() + const loadTopics = async () => { + const ttt = await apiClient.getAllTopics() + await saveTopicsToIndexedDB(db(), ttt) + return ttt + } + onMount(async () => { const db = await setupIndexedDB() - let topics = getTopicsFromIndexedDB(db) + setDb(db) + let { topics, timestamp } = await getTopicsFromIndexedDB(db) - if (topics.length === 0) { - topics = await apiClient.getAllTopics() - await saveTopicsToIndexedDB(db, topics) + if (topics.length < 100 || Date.now() - timestamp > 3600000) { + const newTopics = await loadTopics() + await saveTopicsToIndexedDB(db, newTopics) + topics = newTopics } + addTopics(topics) setRandomTopics(topics) }) - const value: TopicsContextType = { topics: randomTopics } + const value: TopicsContextType = { + setTopicsSort: setSortAllBy, + topicEntities, + sortedTopics, + randomTopics, + topTopics, + addTopics, + loadTopics, + } return {props.children} } diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts index e80f2e6d..bbfa00f4 100644 --- a/src/graphql/client/core.ts +++ b/src/graphql/client/core.ts @@ -1,6 +1,5 @@ import type { Author, - AuthorFollowsResult, CommonResult, FollowingEntity, LoadShoutsOptions, @@ -134,7 +133,7 @@ export const apiClient = { slug?: string author_id?: number user?: string - }): Promise => { + }): Promise => { const response = await publicGraphQLClient.query(authorFollows, params).toPromise() return response.data.get_author_follows }, @@ -175,7 +174,7 @@ export const apiClient = { console.debug('[graphql.client.core] deleteShout:', response) }, - getDrafts: async (): Promise => { + getDrafts: async (): Promise => { const response = await apiClient.private.query(draftsLoad, {}).toPromise() console.debug('[graphql.client.core] getDrafts:', response) return response.data.get_shouts_drafts diff --git a/src/graphql/query/core/articles-load-drafts.ts b/src/graphql/query/core/articles-load-drafts.ts index f81d0a2f..136a14bf 100644 --- a/src/graphql/query/core/articles-load-drafts.ts +++ b/src/graphql/query/core/articles-load-drafts.ts @@ -3,40 +3,43 @@ import { gql } from '@urql/core' export default gql` query LoadDraftsQuery { get_shouts_drafts { - id - title - subtitle - slug - layout - cover - # community - media - main_topic - topics { + error + shouts { id title - body + subtitle slug - stat { - shouts - authors - followers + layout + cover + # community + media + main_topic + topics { + id + title + body + slug + stat { + shouts + authors + followers + } + } + authors { + id + name + slug + pic + created_at } - } - authors { - id - name - slug - pic created_at - } - created_at - published_at - featured_at - stat { - viewed - rating - commented + published_at + featured_at + stat { + viewed + rating + commented + } } } } diff --git a/src/graphql/query/core/author-follows.ts b/src/graphql/query/core/author-follows.ts index 9d46ea2c..31924246 100644 --- a/src/graphql/query/core/author-follows.ts +++ b/src/graphql/query/core/author-follows.ts @@ -3,6 +3,7 @@ import { gql } from '@urql/core' export default gql` query GetAuthorFollows($slug: String, $user: String, $author_id: Int) { get_author_follows(slug: $slug, user: $user, author_id: $author_id) { + error authors { id slug diff --git a/src/graphql/query/core/my-followed.ts b/src/graphql/query/core/my-followed.ts deleted file mode 100644 index f33808fd..00000000 --- a/src/graphql/query/core/my-followed.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { gql } from '@urql/core' - -export default gql` - query MySubscriptionsQuery { - get_my_followed { - topics { - id - title - body - slug - } - authors { - id - name - slug - pic - created_at - } - communities { - id - name - slug - pic - created_at - } - } - } -` diff --git a/src/pages/about/discussionRules.page.tsx b/src/pages/about/discussionRules.page.tsx index f6b2019b..917beb61 100644 --- a/src/pages/about/discussionRules.page.tsx +++ b/src/pages/about/discussionRules.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { StaticPage } from '../../components/Views/StaticPage' import { useLocalize } from '../../context/localize' diff --git a/src/pages/about/dogma.page.tsx b/src/pages/about/dogma.page.tsx index 8fa5130b..ada689a5 100644 --- a/src/pages/about/dogma.page.tsx +++ b/src/pages/about/dogma.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { StaticPage } from '../../components/Views/StaticPage' import { useLocalize } from '../../context/localize' diff --git a/src/pages/about/guide.page.tsx b/src/pages/about/guide.page.tsx index 95e34f5f..c2cba232 100644 --- a/src/pages/about/guide.page.tsx +++ b/src/pages/about/guide.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { StaticPage } from '../../components/Views/StaticPage' import { useLocalize } from '../../context/localize' diff --git a/src/pages/about/help.page.tsx b/src/pages/about/help.page.tsx index 5a0733f3..18a5a642 100644 --- a/src/pages/about/help.page.tsx +++ b/src/pages/about/help.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { Donate } from '../../components/Discours/Donate' import { StaticPage } from '../../components/Views/StaticPage' diff --git a/src/pages/about/manifest.page.tsx b/src/pages/about/manifest.page.tsx index fdcbd20d..7599bdce 100644 --- a/src/pages/about/manifest.page.tsx +++ b/src/pages/about/manifest.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { Feedback } from '../../components/Discours/Feedback' import { Modal } from '../../components/Nav/Modal' diff --git a/src/pages/about/partners.page.tsx b/src/pages/about/partners.page.tsx index 64038137..7277cbb3 100644 --- a/src/pages/about/partners.page.tsx +++ b/src/pages/about/partners.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { StaticPage } from '../../components/Views/StaticPage' import { useLocalize } from '../../context/localize' diff --git a/src/pages/about/principles.page.tsx b/src/pages/about/principles.page.tsx index a5f102ea..d8b10b63 100644 --- a/src/pages/about/principles.page.tsx +++ b/src/pages/about/principles.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { StaticPage } from '../../components/Views/StaticPage' import { useLocalize } from '../../context/localize' diff --git a/src/pages/about/termsOfUse.page.tsx b/src/pages/about/termsOfUse.page.tsx index 7811ed96..a5082158 100644 --- a/src/pages/about/termsOfUse.page.tsx +++ b/src/pages/about/termsOfUse.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { StaticPage } from '../../components/Views/StaticPage' import { useLocalize } from '../../context/localize' diff --git a/src/pages/about/thanks.page.tsx b/src/pages/about/thanks.page.tsx index 0fccade7..4336174d 100644 --- a/src/pages/about/thanks.page.tsx +++ b/src/pages/about/thanks.page.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@solidjs/meta' +import { Meta } from '../../context/meta' import { StaticPage } from '../../components/Views/StaticPage' import { useLocalize } from '../../context/localize' diff --git a/src/pages/allTopics.page.tsx b/src/pages/allTopics.page.tsx index 49d54626..5b440afe 100644 --- a/src/pages/allTopics.page.tsx +++ b/src/pages/allTopics.page.tsx @@ -1,29 +1,15 @@ -import type { PageProps } from './types' - -import { createSignal, onMount } from 'solid-js' - import { AllTopics } from '../components/Views/AllTopics' import { PageLayout } from '../components/_shared/PageLayout' import { useLocalize } from '../context/localize' -import { loadAllTopics } from '../stores/zine/topics' +import { useTopics } from '../context/topics' -export const AllTopicsPage = (props: PageProps) => { +export const AllTopicsPage = () => { const { t } = useLocalize() - - const [isLoaded, setIsLoaded] = createSignal(Boolean(props.allTopics)) - - onMount(async () => { - if (isLoaded()) { - return - } - - await loadAllTopics() - setIsLoaded(true) - }) + const { sortedTopics } = useTopics() return ( - + ) } diff --git a/src/pages/create.page.tsx b/src/pages/create.page.tsx index ea5c27f2..e5b7e8a1 100644 --- a/src/pages/create.page.tsx +++ b/src/pages/create.page.tsx @@ -1,6 +1,6 @@ import { redirectPage } from '@nanostores/router' -import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' +import { Meta } from '../context/meta' import { AuthGuard } from '../components/AuthGuard' import { Button } from '../components/_shared/Button' @@ -18,7 +18,7 @@ import styles from '../styles/Create.module.scss' const handleCreate = async (layout: LayoutType) => { const shout = await apiClient.createArticle({ article: { layout: layout } }) redirectPage(router, 'edit', { - shoutId: shout.id.toString(), + shoutId: shout?.id.toString(), }) } diff --git a/src/pages/edit.page.tsx b/src/pages/edit.page.tsx index ca6cd445..21353681 100644 --- a/src/pages/edit.page.tsx +++ b/src/pages/edit.page.tsx @@ -1,9 +1,10 @@ -import { Show, Suspense, createMemo, createSignal, lazy, onMount } from 'solid-js' +import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js' import { AuthGuard } from '../components/AuthGuard' import { Loading } from '../components/_shared/Loading' import { PageLayout } from '../components/_shared/PageLayout' import { useLocalize } from '../context/localize' +import { useSession } from '../context/session' import { apiClient } from '../graphql/client/core' import { Shout } from '../graphql/schema/core.gen' import { router } from '../stores/router' @@ -14,67 +15,71 @@ import { LayoutType } from './types' const EditView = lazy(() => import('../components/Views/EditView/EditView')) -export const EditPage = () => { - const snackbar = useSnackbar() - const { t } = useLocalize() +const getContentTypeTitle = (layout: LayoutType) => { + switch (layout) { + case 'audio': + return 'Publish Album' + case 'image': + return 'Create gallery' + case 'video': + return 'Create video' + case 'literature': + return 'New literary work' + default: + return 'Write an article' + } +} - const [shout, setShout] = createSignal(null) - const loadMyShout = async (shout_id: number) => { - if (shout_id) { - const { shout: loadedShout, error } = await apiClient.getMyShout(shout_id) - console.log(loadedShout) - if (error) { - await snackbar?.showSnackbar({ type: 'error', body: t('This content is not published yet') }) - redirectPage(router, 'drafts') - } else { - setShout(loadedShout) - } - } +export const EditPage = () => { + const { t } = useLocalize() + const { session } = useSession() + const snackbar = useSnackbar() + + const fail = async (error: string) => { + console.error(error) + const errorMessage = error === 'forbidden' ? "You can't edit this post" : error + await snackbar?.showSnackbar({ type: 'error', body: t(errorMessage) }) + redirectPage(router, 'drafts') } - onMount(async () => { - const shout_id = window.location.pathname.split('/').pop() - if (shout_id) { - try { - await loadMyShout(Number.parseInt(shout_id, 10)) - } catch (e) { - console.error(e) - } - } + const [shoutId, setShoutId] = createSignal(0) + const [shout, setShout] = createSignal() + + onMount(() => { + const shoutId = window.location.pathname.split('/').pop() + const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10) + console.debug(`editing shout ${shoutIdFromUrl}`) + if (shoutIdFromUrl) setShoutId(shoutIdFromUrl) }) + createEffect( + on([session, shout, shoutId], async ([ses, sh, shid]) => { + if (ses?.user && !sh && shid) { + const { shout: loadedShout, error } = await apiClient.getMyShout(shid) + if (error) { + fail(error) + } else { + setShout(loadedShout) + } + } + }), + ) + const title = createMemo(() => { if (!shout()) { return t('Create post') } - - switch (shout().layout as LayoutType) { - case 'audio': { - return t('Publish Album') - } - case 'image': { - return t('Create gallery') - } - case 'video': { - return t('Create video') - } - case 'literature': { - return t('New literary work') - } - default: { - return t('Write an article') - } - } + return t(getContentTypeTitle(shout()?.layout as LayoutType)) }) return ( - - }> - - - + }> + }> + + + ) diff --git a/src/pages/index.page.tsx b/src/pages/index.page.tsx index 93d3c412..a04c09b0 100644 --- a/src/pages/index.page.tsx +++ b/src/pages/index.page.tsx @@ -2,13 +2,12 @@ import type { PageProps } from './types' import { Show, createSignal, onCleanup, onMount } from 'solid-js' -import { HomeView, PRERENDERED_ARTICLES_COUNT, RANDOM_TOPICS_COUNT } from '../components/Views/Home' +import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../components/Views/Home' import { Loading } from '../components/_shared/Loading' import { PageLayout } from '../components/_shared/PageLayout' import { useLocalize } from '../context/localize' import { ReactionsProvider } from '../context/reactions' import { loadShouts, resetSortedArticles } from '../stores/zine/articles' -import { loadRandomTopics } from '../stores/zine/topics' export const HomePage = (props: PageProps) => { const [isLoaded, setIsLoaded] = createSignal(Boolean(props.homeShouts)) @@ -19,10 +18,7 @@ export const HomePage = (props: PageProps) => { return } - await Promise.all([ - loadShouts({ filters: { featured: true }, limit: PRERENDERED_ARTICLES_COUNT }), - loadRandomTopics({ amount: RANDOM_TOPICS_COUNT }), - ]) + await loadShouts({ filters: { featured: true }, limit: PRERENDERED_ARTICLES_COUNT }) setIsLoaded(true) }) diff --git a/src/pages/profile/Settings.module.scss b/src/pages/profile/Settings.module.scss index 9bd4906c..38babb4d 100644 --- a/src/pages/profile/Settings.module.scss +++ b/src/pages/profile/Settings.module.scss @@ -100,17 +100,6 @@ h5 { } } -.passwordToggleControl { - position: absolute; - right: 1em; - transform: translateY(-50%); - top: 50%; -} - -.passwordInput { - padding-right: 3em !important; -} - .searchContainer { margin-top: 2.4rem; } @@ -331,3 +320,12 @@ div[data-lastpass-infield="true"] { opacity: 0 !important; } + +.emailValidationError { + position: absolute; + top: 100%; + font-size: 12px; + line-height: 16px; + margin-top: 0.3em; + color: var(--danger-color); +} diff --git a/src/pages/profile/profileSecurity.page.tsx b/src/pages/profile/profileSecurity.page.tsx index 2e572047..b19f139e 100644 --- a/src/pages/profile/profileSecurity.page.tsx +++ b/src/pages/profile/profileSecurity.page.tsx @@ -6,135 +6,321 @@ import { Icon } from '../../components/_shared/Icon' import { PageLayout } from '../../components/_shared/PageLayout' import { useLocalize } from '../../context/localize' +import { UpdateProfileInput } from '@authorizerdev/authorizer-js' +import { Show, createEffect, createSignal, on } from 'solid-js' +import { PasswordField } from '../../components/Nav/AuthModal/PasswordField' +import { Button } from '../../components/_shared/Button' +import { Loading } from '../../components/_shared/Loading' +import { useConfirm } from '../../context/confirm' +import { useSession } from '../../context/session' +import { useSnackbar } from '../../context/snackbar' +import { DEFAULT_HEADER_OFFSET } from '../../stores/router' +import { validateEmail } from '../../utils/validateEmail' import styles from './Settings.module.scss' +type FormField = 'oldPassword' | 'newPassword' | 'newPasswordConfirm' | 'email' export const ProfileSecurityPage = () => { const { t } = useLocalize() + const { updateProfile, session, isSessionLoaded } = useSession() + const { showSnackbar } = useSnackbar() + const { showConfirm } = useConfirm() + + const [newPasswordError, setNewPasswordError] = createSignal() + const [oldPasswordError, setOldPasswordError] = createSignal() + const [emailError, setEmailError] = createSignal() + const [isSubmitting, setIsSubmitting] = createSignal() + const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false) + + const initialState = { + oldPassword: undefined, + newPassword: undefined, + newPasswordConfirm: undefined, + email: undefined, + } + const [formData, setFormData] = createSignal(initialState) + const oldPasswordRef: { current: HTMLDivElement } = { current: null } + const newPasswordRepeatRef: { current: HTMLDivElement } = { current: null } + + createEffect( + on( + () => session()?.user?.email, + () => { + setFormData((prevData) => ({ + ...prevData, + ['email']: session()?.user?.email, + })) + }, + ), + ) + const handleInputChange = (name: FormField, value: string) => { + if ( + name === 'email' || + (name === 'newPasswordConfirm' && value && value?.length > 0 && !emailError() && !newPasswordError()) + ) { + setIsFloatingPanelVisible(true) + } else { + setIsFloatingPanelVisible(false) + } + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })) + } + + const handleCancel = async () => { + const isConfirmed = await showConfirm({ + confirmBody: t('Do you really want to reset all changes?'), + confirmButtonVariant: 'primary', + declineButtonVariant: 'secondary', + }) + if (isConfirmed) { + setEmailError() + setFormData({ + ...initialState, + ['email']: session()?.user?.email, + }) + setIsFloatingPanelVisible(false) + } + } + const handleChangeEmail = (_value: string) => { + if (!validateEmail(formData()['email'])) { + setEmailError(t('Invalid email')) + return + } + } + const handleCheckNewPassword = (value: string) => { + handleInputChange('newPasswordConfirm', value) + if (value !== formData()['newPassword']) { + const rect = newPasswordRepeatRef.current.getBoundingClientRect() + const topPosition = window.scrollY + rect.top - DEFAULT_HEADER_OFFSET * 2 + window.scrollTo({ + top: topPosition, + left: 0, + behavior: 'smooth', + }) + showSnackbar({ type: 'error', body: t('Incorrect new password confirm') }) + setNewPasswordError(t('Passwords are not equal')) + } + } + + const handleSubmit = async () => { + setIsSubmitting(true) + + const options: UpdateProfileInput = { + old_password: formData()['oldPassword'], + new_password: formData()['newPassword'] || formData()['oldPassword'], + confirm_new_password: formData()['newPassword'] || formData()['oldPassword'], + email: formData()['email'], + } + + try { + const { errors } = await updateProfile(options) + if (errors.length > 0) { + console.error(errors) + if (errors.some((obj) => obj.message === 'incorrect old password')) { + setOldPasswordError(t('Incorrect old password')) + showSnackbar({ type: 'error', body: t('Incorrect old password') }) + const rect = oldPasswordRef.current.getBoundingClientRect() + const topPosition = window.scrollY + rect.top - DEFAULT_HEADER_OFFSET * 2 + window.scrollTo({ + top: topPosition, + left: 0, + behavior: 'smooth', + }) + setIsFloatingPanelVisible(false) + } + return + } + showSnackbar({ type: 'success', body: t('Profile successfully saved') }) + } catch (error) { + console.error(error) + } finally { + setIsSubmitting(false) + } + } return ( -
          -
          -
          -
          - + }> +
          +
          +
          +
          + +
          -
          -
          -
          -
          -

          Вход и безопасность

          -

          Настройки аккаунта, почты, пароля и способов входа.

          - -
          -

          Почта

          -
          - - -
          - -

          Изменить пароль

          -
          Текущий пароль
          -
          - - -
          - -
          Новый пароль
          -
          - - -
          - -
          Подтвердите новый пароль
          -
          - - -
          - -

          Социальные сети

          -
          Google
          -
          -

          - -

          -
          - -
          VK
          -
          -

          - -

          -
          - -
          Facebook
          -
          -

          - -

          -
          - -
          Apple
          -
          -

          - -

          -
          - -
          -

          - +

          +
          +
          +

          {t('Login and security')}

          +

          + {t('Settings for account, email, password and login methods.')}

          - + +
          +

          {t('Email')}

          +
          + setEmailError()} + onInput={(event) => handleChangeEmail(event.target.value)} + /> + + +
          + {emailError()} +
          +
          +
          + +

          {t('Change password')}

          +
          {t('Current password')}
          + +
          (oldPasswordRef.current = el)}> + setOldPasswordError()} + setError={oldPasswordError()} + onInput={(value) => handleInputChange('oldPassword', value)} + value={formData()['oldPassword'] ?? null} + disabled={isSubmitting()} + /> +
          + +
          {t('New password')}
          + { + handleInputChange('newPassword', value) + handleInputChange('newPasswordConfirm', '') + }} + value={formData()['newPassword'] ?? ''} + disabled={isSubmitting()} + disableAutocomplete={true} + /> + +
          {t('Confirm your new password')}
          +
          (newPasswordRepeatRef.current = el)}> + 0 + ? formData()['newPasswordConfirm'] + : null + } + onFocus={() => setNewPasswordError()} + setError={newPasswordError()} + onInput={(value) => handleCheckNewPassword(value)} + disabled={isSubmitting()} + disableAutocomplete={true} + /> +
          +

          {t('Social networks')}

          +
          Google
          +
          +

          + +

          +
          + +
          VK
          +
          +

          + +

          +
          + +
          Facebook
          +
          +

          + +

          +
          + +
          Apple
          +
          +

          + +

          +
          + +
          -
          + + + +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          ) diff --git a/src/pages/topic.page.tsx b/src/pages/topic.page.tsx index b5e6212a..4fa3fb49 100644 --- a/src/pages/topic.page.tsx +++ b/src/pages/topic.page.tsx @@ -8,7 +8,6 @@ import { PageLayout } from '../components/_shared/PageLayout' import { ReactionsProvider } from '../context/reactions' import { useRouter } from '../stores/router' import { loadShouts, resetSortedArticles } from '../stores/zine/articles' -import { loadTopic } from '../stores/zine/topics' export const TopicPage = (props: PageProps) => { const { page } = useRouter() @@ -25,7 +24,6 @@ export const TopicPage = (props: PageProps) => { limit: PRERENDERED_ARTICLES_COUNT, offset: 0, }), - loadTopic({ slug: slug() }), ]) onMount(async () => { diff --git a/src/renderer/_default.page.client.tsx b/src/renderer/_default.page.client.tsx index fff76e88..8b74aca6 100644 --- a/src/renderer/_default.page.client.tsx +++ b/src/renderer/_default.page.client.tsx @@ -1,7 +1,7 @@ import type { PageContextBuiltInClientWithClientRouting } from 'vike/types' import type { PageContext } from './types' -// import * as Sentry from '@sentry/browser' +import { init as SentryInit, replayIntegration } from '@sentry/browser' import i18next from 'i18next' import HttpApi from 'i18next-http-backend' import ICU from 'i18next-icu' @@ -9,7 +9,7 @@ import { hydrate } from 'solid-js/web' import { App } from '../components/App' import { initRouter } from '../stores/router' -// import { SENTRY_DSN } from '../utils/config' +import { GLITCHTIP_DSN } from '../utils/config' import { resolveHydrationPromise } from '../utils/hydrationPromise' let layoutReady = false @@ -20,13 +20,16 @@ export const render = async (pageContext: PageContextBuiltInClientWithClientRout const { pathname, search } = window.location const searchParams = Object.fromEntries(new URLSearchParams(search)) initRouter(pathname, searchParams) - /* - if (SENTRY_DSN) { - Sentry.init({ - dsn: SENTRY_DSN, - }) - } - */ + + SentryInit({ + dsn: GLITCHTIP_DSN, + tracesSampleRate: 0.01, + integrations: [replayIntegration()], + // Session Replay + replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. + replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. + }) + // eslint-disable-next-line import/no-named-as-default-member await i18next .use(HttpApi) diff --git a/src/stores/router.ts b/src/stores/router.ts index 3b2e49c8..46a5efd1 100644 --- a/src/stores/router.ts +++ b/src/stores/router.ts @@ -69,6 +69,7 @@ const checkOpenOnClient = (link: HTMLAnchorElement, event) => { ) } +// TODO: use scrollToHash or remove const _scrollToHash = (hash: string) => { let selector = hash diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts index 7098ad2b..677af2a2 100644 --- a/src/stores/zine/authors.ts +++ b/src/stores/zine/authors.ts @@ -6,7 +6,7 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen' export type AuthorsSortBy = 'shouts' | 'name' | 'followers' type SortedAuthorsSetter = (prev: Author[]) => Author[] - +// FIXME: use signal or remove const [_sortAllBy, setSortAllBy] = createSignal('name') export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy) diff --git a/src/stores/zine/seen.ts b/src/stores/zine/seen.ts deleted file mode 100644 index bf01d92d..00000000 --- a/src/stores/zine/seen.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createStorageSignal } from '@solid-primitives/storage' - -// TODO: use indexedDB here -export const [seen, setSeen] = createStorageSignal<{ [slug: string]: Date }>('seen', {}) -export const addSeen = (slug) => setSeen({ ...seen(), [slug]: Date.now() }) - -export const useSeenStore = (initialData: { [slug: string]: Date } = {}) => { - setSeen({ ...seen(), ...initialData }) - - return { - seen, - setSeen, - addSeen, - } -} diff --git a/src/stores/zine/topics.ts b/src/stores/zine/topics.ts deleted file mode 100644 index 6de17d64..00000000 --- a/src/stores/zine/topics.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { Topic } from '../../graphql/schema/core.gen' - -import { createLazyMemo } from '@solid-primitives/memo' -import { createMemo, createSignal } from 'solid-js' - -import { apiClient } from '../../graphql/client/core' -import { byTopicStatDesc } from '../../utils/sortby' -import { useRouter } from '../router' - -export type TopicsSortBy = 'followers' | 'title' | 'authors' | 'shouts' - -const [sortAllBy, setSortAllBy] = createSignal('shouts') - -export const setTopicsSort = (sortBy: TopicsSortBy) => setSortAllBy(sortBy) - -const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({}) -const [randomTopics, setRandomTopics] = createSignal([]) - -const sortedTopics = createLazyMemo(() => { - const topics = Object.values(topicEntities()) - const { changeSearchParams } = useRouter() - switch (sortAllBy()) { - case 'followers': { - // console.debug('[store.topics] sorted by followers') - topics.sort(byTopicStatDesc('followers')) - break - } - case 'shouts': { - // log.debug(`sorted by shouts`) - topics.sort(byTopicStatDesc('shouts')) - break - } - case 'authors': { - // log.debug(`sorted by authors`) - topics.sort(byTopicStatDesc('authors')) - break - } - case 'title': { - // console.debug('[store.topics] sorted by title') - topics.sort((a, b) => a.title.localeCompare(b.title)) - break - } - default: { - topics.sort(byTopicStatDesc('shouts')) - changeSearchParams({ by: 'shouts' }) - } - } - - return topics -}) - -const topTopics = createMemo(() => { - const topics = Object.values(topicEntities()) - topics.sort(byTopicStatDesc('shouts')) - return topics -}) - -const addTopics = (...args: Topic[][]) => { - const allTopics = args.flatMap((topics) => (topics || []).filter(Boolean)) - - const newTopicEntities = allTopics.reduce( - (acc, topic) => { - acc[topic.slug] = topic - return acc - }, - {} as Record, - ) - - setTopicEntities((prevTopicEntities) => { - return { - ...prevTopicEntities, - ...newTopicEntities, - } - }) -} - -export const loadAllTopics = async (): Promise => { - const topics = await apiClient.getAllTopics() - addTopics(topics) -} - -export const loadRandomTopics = async ({ amount }: { amount: number }): Promise => { - const topics = await apiClient.getRandomTopics({ amount }) - setRandomTopics(topics) -} - -export const loadTopic = async ({ slug }: { slug: string }): Promise => { - const topic = await apiClient.getTopic({ slug }) - addTopics([topic]) -} - -type InitialState = { - topics?: Topic[] - randomTopics?: Topic[] - sortBy?: TopicsSortBy -} - -export const useTopicsStore = (initialState: InitialState = {}) => { - if (initialState.sortBy) { - setSortAllBy(initialState.sortBy) - } - - addTopics(initialState.topics, initialState.randomTopics) - - if (initialState.randomTopics) { - setRandomTopics(initialState.randomTopics) - } - - return { topicEntities, sortedTopics, randomTopics, topTopics } -} diff --git a/src/utils/config.ts b/src/utils/config.ts index cfc2f7fa..41710c83 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -5,6 +5,7 @@ export const cdnUrl = 'https://cdn.discours.io' export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || '' +export const GLITCHTIP_DSN = import.meta.env.PUBLIC_GLITCHTIP_DSN || '' const defaultSearchUrl = 'https://search.discours.io' export const searchUrl = import.meta.env.PUBLIC_SEARCH_URL || defaultSearchUrl diff --git a/src/utils/getRandomTopicsFromArray.ts b/src/utils/getRandomTopicsFromArray.ts index ced616c0..fb1a9328 100644 --- a/src/utils/getRandomTopicsFromArray.ts +++ b/src/utils/getRandomTopicsFromArray.ts @@ -2,6 +2,7 @@ import { RANDOM_TOPICS_COUNT } from '../components/Views/Home' import { Topic } from '../graphql/schema/core.gen' export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => { + if (!Array.isArray(topics)) return [] const shuffledTopics = [...topics].sort(() => 0.5 - Math.random()) return shuffledTopics.slice(0, count) } diff --git a/src/utils/handleFileUpload.ts b/src/utils/handleFileUpload.ts index 7013de52..6d7066ed 100644 --- a/src/utils/handleFileUpload.ts +++ b/src/utils/handleFileUpload.ts @@ -5,12 +5,15 @@ import { UploadedFile } from '../pages/types' const apiBaseUrl = 'https://core.discours.io' const apiUrl = `${apiBaseUrl}/upload` -export const handleFileUpload = async (uploadFile: UploadFile): Promise => { +export const handleFileUpload = async (uploadFile: UploadFile, token: string): Promise => { const formData = new FormData() formData.append('file', uploadFile.file, uploadFile.name) const response = await fetch(apiUrl, { method: 'POST', body: formData, + headers: { + Authorization: token, + }, }) return response.json() } diff --git a/src/utils/handleImageUpload.ts b/src/utils/handleImageUpload.ts index 54ec619e..9fd2455f 100644 --- a/src/utils/handleImageUpload.ts +++ b/src/utils/handleImageUpload.ts @@ -4,12 +4,14 @@ import { UploadedFile } from '../pages/types' import { thumborUrl } from './config' -export const handleImageUpload = async (uploadFile: UploadFile): Promise => { +export const handleImageUpload = async (uploadFile: UploadFile, token: string): Promise => { const formData = new FormData() formData.append('media', uploadFile.file, uploadFile.name) + const headers = token ? { Authorization: token } : {} const response = await fetch(`${thumborUrl}/image`, { method: 'POST', body: formData, + headers, }) const location = response.headers.get('Location') diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 10eeb0ee..d436946c 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -1,6 +1,7 @@ import { Shout } from '../graphql/schema/core.gen' const MAX_DESCRIPTION_LENGTH = 150 + export const getDescription = (body: string): string => { if (!body) { return '' diff --git a/vite.config.ts b/vite.config.ts index 6f1389bc..f50a5bab 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ import ssrPlugin from 'vike/plugin' import { defineConfig } from 'vite' import mkcert from 'vite-plugin-mkcert' -import { nodePolyfills } from 'vite-plugin-node-polyfills'; +import { nodePolyfills } from 'vite-plugin-node-polyfills' import sassDts from 'vite-plugin-sass-dts' import solidPlugin from 'vite-plugin-solid' @@ -69,6 +69,7 @@ export default defineConfig(({ mode, command }) => { https: {}, port: 3000, }, + sourcemap: isDev, css: { devSourcemap: isDev, preprocessorOptions: {