diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 34ebf90f..b832f5f6 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -14,3 +14,10 @@ - `fly logs --app annos` - `fly secrets set SUPER_SECRET_KEY=password1234` - make sure that new environment vars are added as secrets with the above command + +## local development with mocked backend + +- `npm install` +- in one terminal: `npm run dev:mock` +- another terminal: `npm run webapp:dev` +- ui is available on `localhost:4000` diff --git a/package-lock.json b/package-lock.json index 8c93903c..89588743 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7784,7 +7784,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "dependencies": { "detect-libc": "^1.0.3", @@ -7822,12 +7822,10 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7843,12 +7841,10 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7864,12 +7860,10 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7885,12 +7879,10 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7906,12 +7898,10 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7927,12 +7917,10 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7948,12 +7936,10 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7969,12 +7955,10 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -7990,12 +7974,10 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -8011,12 +7993,10 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -8032,12 +8012,10 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -8053,12 +8031,10 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -8074,12 +8050,10 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -8092,7 +8066,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, + "devOptional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -11192,7 +11166,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -11209,7 +11182,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -11226,7 +11198,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -11243,7 +11214,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -11260,7 +11230,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -11277,7 +11246,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -11294,7 +11262,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -11311,7 +11278,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -11328,7 +11294,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -11345,7 +11310,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -14099,7 +14063,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "devOptional": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -15532,48 +15496,6 @@ "node": ">=4" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "license": "CC0-1.0", - "optional": true, - "peer": true - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -16704,7 +16626,6 @@ "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, "optional": true, - "peer": true, "dependencies": { "prr": "~1.0.1" }, @@ -18124,7 +18045,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "devOptional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -19203,7 +19124,6 @@ "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "dev": true, "optional": true, - "peer": true, "bin": { "image-size": "bin/image-size.js" }, @@ -19629,7 +19549,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -19696,7 +19616,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -19747,7 +19667,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.12.0" } @@ -21183,7 +21103,6 @@ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, "optional": true, - "peer": true, "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" @@ -21198,7 +21117,6 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "optional": true, - "peer": true, "bin": { "mime": "cli.js" }, @@ -21212,7 +21130,6 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "optional": true, - "peer": true, "bin": { "semver": "bin/semver" } @@ -21223,7 +21140,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -21291,7 +21207,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21312,7 +21227,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21333,7 +21247,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21354,7 +21267,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21375,7 +21287,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21396,7 +21307,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21417,7 +21327,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21438,7 +21347,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21459,7 +21367,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21480,7 +21387,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -21501,7 +21407,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -22674,7 +22579,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "devOptional": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -22687,7 +22592,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -22970,7 +22875,6 @@ "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "optional": true, - "peer": true, "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" @@ -23318,7 +23222,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true + "devOptional": true }, "node_modules/node-fetch": { "version": "2.7.0", @@ -24318,7 +24222,6 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=6" } @@ -25600,8 +25503,7 @@ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/pstree.remy": { "version": "1.1.8", @@ -28007,46 +27909,6 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, - "node_modules/svgo": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", - "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0", - "sax": "^1.5.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -28453,7 +28315,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/src/modules/cards/infrastructure/IFramelyMetadataService.ts b/src/modules/cards/infrastructure/IFramelyMetadataService.ts index 6031e05c..18a43638 100644 --- a/src/modules/cards/infrastructure/IFramelyMetadataService.ts +++ b/src/modules/cards/infrastructure/IFramelyMetadataService.ts @@ -37,16 +37,24 @@ interface IFramelyResponse { export class IFramelyMetadataService implements IMetadataService { private readonly baseUrl = 'https://iframe.ly/api/iframely'; - private readonly apiKey: string; + private readonly apiKey: string | null; constructor(apiKey: string) { if (!apiKey || apiKey.trim().length === 0) { - throw new Error('Iframely API key is required'); + console.warn( + 'IFramelyMetadataService: No API key provided. Metadata fetching will be unavailable.', + ); + this.apiKey = null; + } else { + this.apiKey = apiKey; } - this.apiKey = apiKey; } async fetchMetadata(url: URL): Promise> { + if (!this.apiKey) { + return err(new Error('Iframely API key not configured')); + } + try { const encodedUrl = encodeURIComponent(url.value); const fullUrl = `${this.baseUrl}?url=${encodedUrl}&api_key=${this.apiKey}`; @@ -108,6 +116,10 @@ export class IFramelyMetadataService implements IMetadataService { } async isAvailable(): Promise { + if (!this.apiKey) { + return false; + } + try { // Test with a simple URL to check if the service is available const testUrl = `${this.baseUrl}?url=${encodeURIComponent('https://example.com')}&api_key=${this.apiKey}`; diff --git a/src/webapp/features/cards/components/addCardDrawer/AddCardForm.tsx b/src/webapp/features/cards/components/addCardDrawer/AddCardForm.tsx index dc81c2a1..a8952d69 100644 --- a/src/webapp/features/cards/components/addCardDrawer/AddCardForm.tsx +++ b/src/webapp/features/cards/components/addCardDrawer/AddCardForm.tsx @@ -33,7 +33,6 @@ import { Collection, CollectionAccessType } from '@semble/types'; import { FaSeedling } from 'react-icons/fa6'; import { CardSaveSource } from '@/features/analytics/types'; import { usePathname } from 'next/navigation'; -import { BsCheck, BsExclamation } from 'react-icons/bs'; interface Props { onClose: () => void; @@ -112,33 +111,7 @@ export default function AddCardForm(props: Props) { setSelectedCollections(initialCollections); form.reset(); - addCard.mutate(cardData, { - onSuccess: () => { - notifications.update({ - id: notificationId, - color: 'green', - title: 'Success!', - message: 'Card added', - position: 'top-center', - loading: false, - autoClose: 2000, - icon: , - }); - }, - onError: () => { - notifications.update({ - id: notificationId, - color: 'red', - title: 'Error', - message: 'Could not add card', - position: 'top-center', - loading: false, - autoClose: 5000, - withCloseButton: true, - icon: , - }); - }, - }); + addCard.mutate({ ...cardData, notificationId }); }; return ( diff --git a/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx b/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx index e5796306..c8708b85 100644 --- a/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx +++ b/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx @@ -10,7 +10,6 @@ import useAddCard from '@/features/cards/lib/mutations/useAddCard'; import useUpdateCardAssociations from '@/features/cards/lib/mutations/useUpdateCardAssociations'; import { notifications } from '@mantine/notifications'; import { track } from '@vercel/analytics'; -import { BsCheck, BsExclamation } from 'react-icons/bs'; interface Props { isOpen: boolean; @@ -78,33 +77,7 @@ export default function AddCardToModal(props: Props) { props.onClose(); - addCard.mutate(data.cardData, { - onSuccess: () => { - notifications.update({ - id: notificationId, - color: 'green', - title: 'Success!', - message: 'Card added', - position: 'top-center', - loading: false, - autoClose: 2000, - icon: , - }); - }, - onError: () => { - notifications.update({ - id: notificationId, - color: 'red', - title: 'Error', - message: 'Could not add card', - loading: false, - autoClose: 5000, - withCloseButton: true, - position: 'top-center', - icon: , - }); - }, - }); + addCard.mutate({ ...data.cardData, notificationId }); } else if (!data.isAddingNewCard && data.updateData) { const notificationId = `update-card-${Date.now()}`; notifications.show({ @@ -119,33 +92,7 @@ export default function AddCardToModal(props: Props) { props.onClose(); - updateCardAssociations.mutate(data.updateData, { - onSuccess: () => { - notifications.update({ - id: notificationId, - color: 'green', - title: 'Success!', - message: 'Card updated', - position: 'top-center', - loading: false, - autoClose: 2000, - icon: , - }); - }, - onError: () => { - notifications.update({ - id: notificationId, - color: 'red', - title: 'Error', - message: 'Could not update card', - position: 'top-center', - loading: false, - autoClose: false, - withCloseButton: true, - icon: , - }); - }, - }); + updateCardAssociations.mutate({ ...data.updateData, notificationId }); } }; diff --git a/src/webapp/features/cards/lib/mutations/useAddCard.tsx b/src/webapp/features/cards/lib/mutations/useAddCard.tsx index d9ca9bb4..9fc06bdf 100644 --- a/src/webapp/features/cards/lib/mutations/useAddCard.tsx +++ b/src/webapp/features/cards/lib/mutations/useAddCard.tsx @@ -11,6 +11,8 @@ import { CardSaveEventProperties, } from '@/features/analytics/types'; import { shouldCaptureAnalytics } from '@/features/analytics/utils'; +import { notifications } from '@mantine/notifications'; +import { BsCheck, BsExclamation } from 'react-icons/bs'; export default function useAddCard( analyticsContext?: CardSaveAnalyticsContext, @@ -23,6 +25,7 @@ export default function useAddCard( note?: string; collectionIds?: string[]; viaCardId?: string; + notificationId?: string; }) => { return addUrlToLibrary(newCard.url, { note: newCard.note, @@ -31,10 +34,25 @@ export default function useAddCard( }); }, - // Do things that are absolutely necessary and logic related (like query invalidation) in the useMutation callbacks - // Do UI related things like redirects or showing toast notifications in mutate callbacks. If the user navigated away from the current screen before the mutation finished, those will purposefully not fire + // Generally, do UI things (redirects, toasts) in mutate-level callbacks so they + // don't fire if the user navigated away. But loading toasts that need .update() + // must be handled here — Suspense re-renders can unmount the caller and drop + // mutate-level callbacks, leaving the loading toast stuck forever. // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire onSuccess: (_data, variables) => { + if (variables.notificationId) { + notifications.update({ + id: variables.notificationId, + color: 'green', + title: 'Success!', + message: 'Card added', + position: 'top-center', + loading: false, + autoClose: 2000, + icon: , + }); + } + queryClient.invalidateQueries({ queryKey: cardKeys.all() }); queryClient.invalidateQueries({ queryKey: noteKeys.all() }); queryClient.invalidateQueries({ queryKey: feedKeys.all() }); @@ -86,6 +104,22 @@ export default function useAddCard( posthog.unregister('original_active_filters'); } }, + + onError: (_error, variables) => { + if (variables.notificationId) { + notifications.update({ + id: variables.notificationId, + color: 'red', + title: 'Error', + message: 'Could not add card', + position: 'top-center', + loading: false, + autoClose: 5000, + withCloseButton: true, + icon: , + }); + } + }, }); return mutation; diff --git a/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx b/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx index 89dddcf9..94736b3b 100644 --- a/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx +++ b/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx @@ -11,6 +11,8 @@ import { CardSaveEventProperties, } from '@/features/analytics/types'; import { shouldCaptureAnalytics } from '@/features/analytics/utils'; +import { notifications } from '@mantine/notifications'; +import { BsCheck, BsExclamation } from 'react-icons/bs'; export default function useUpdateCardAssociations( analyticsContext?: CardSaveAnalyticsContext, @@ -27,6 +29,7 @@ export default function useUpdateCardAssociations( removeFromCollectionIds?: string[]; viaCardId?: string; addToLibrary?: boolean; + notificationId?: string; }) => { return client.updateUrlCardAssociations({ cardId: updatedCard.cardId, @@ -38,6 +41,19 @@ export default function useUpdateCardAssociations( }, onSuccess: (_data, variables) => { + if (variables.notificationId) { + notifications.update({ + id: variables.notificationId, + color: 'green', + title: 'Success!', + message: 'Card updated', + position: 'top-center', + loading: false, + autoClose: 2000, + icon: , + }); + } + queryClient.invalidateQueries({ queryKey: cardKeys.all() }); queryClient.invalidateQueries({ queryKey: noteKeys.all() }); queryClient.invalidateQueries({ queryKey: feedKeys.all() }); @@ -101,6 +117,22 @@ export default function useUpdateCardAssociations( posthog.unregister('original_active_filters'); } }, + + onError: (_error, variables) => { + if (variables.notificationId) { + notifications.update({ + id: variables.notificationId, + color: 'red', + title: 'Error', + message: 'Could not update card', + position: 'top-center', + loading: false, + autoClose: false, + withCloseButton: true, + icon: , + }); + } + }, }); return mutation; diff --git a/src/webapp/features/composer/components/Composer.tsx b/src/webapp/features/composer/components/Composer.tsx index d008bd52..7a31d628 100644 --- a/src/webapp/features/composer/components/Composer.tsx +++ b/src/webapp/features/composer/components/Composer.tsx @@ -38,7 +38,7 @@ import { FaSeedling } from 'react-icons/fa6'; import { FaRegNoteSticky } from 'react-icons/fa6'; import { CardSaveSource } from '@/features/analytics/types'; import { usePathname } from 'next/navigation'; -import { BsCheck, BsExclamation } from 'react-icons/bs'; +import { BsExclamation } from 'react-icons/bs'; import AddConnectionForm from '@/features/connections/components/addConnectionDrawer/AddConnectionForm'; import { TbPlugConnected } from 'react-icons/tb'; @@ -156,33 +156,7 @@ export default function Composer(props: Props) { window.history.replaceState({}, '', window.location.pathname); cardForm.reset(); - addCard.mutate(cardData, { - onSuccess: () => { - notifications.update({ - id: notificationId, - color: 'green', - title: 'Success!', - message: 'Card added', - position: 'top-center', - loading: false, - autoClose: 2000, - icon: , - }); - }, - onError: () => { - notifications.update({ - id: notificationId, - color: 'red', - title: 'Error', - message: 'Could not add card', - position: 'top-center', - loading: false, - autoClose: 5000, - withCloseButton: true, - icon: , - }); - }, - }); + addCard.mutate({ ...cardData, notificationId }); }; const handleCreateCollection = (e: React.FormEvent) => {