diff --git a/package-lock.json b/package-lock.json index 12fe99f..1a2f560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@tanstack/react-query": "^5.100.9", - "@tauri-apps/api": "^2.11.0", - "@tauri-apps/plugin-opener": "^2.5.4", - "@tauri-apps/plugin-process": "^2.3.1", - "@tauri-apps/plugin-updater": "^2.10.1", + "@tauri-apps/api": "2.10.1", + "@tauri-apps/plugin-opener": "2.5.4", + "@tauri-apps/plugin-process": "2.3.1", + "@tauri-apps/plugin-updater": "2.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -36,7 +36,10 @@ "@eslint/js": "^9.39.4", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.3.0", - "@tauri-apps/cli": "^2.11.1", + "@tauri-apps/cli": "2.10.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/markdown-to-jsx": "^7.0.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -46,15 +49,24 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "jsdom": "^29.1.1", "prettier": "^3.8.3", "shadcn": "^3.8.5", "tailwindcss": "^4.3.0", "tw-animate-css": "^1.4.0", "typescript": "~5.8.3", "typescript-eslint": "^8.59.2", - "vite": "^7.3.3" + "vite": "^7.3.3", + "vitest": "^4.1.6" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@antfu/ni": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-25.0.0.tgz", @@ -77,6 +89,57 @@ "nup": "bin/nup.mjs" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -494,6 +557,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -542,6 +615,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@date-fns/tz": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", @@ -1329,6 +1555,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -4046,9 +4290,9 @@ } }, "node_modules/@tauri-apps/api": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", - "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", @@ -4056,9 +4300,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz", - "integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -4072,23 +4316,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.1", - "@tauri-apps/cli-darwin-x64": "2.11.1", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.1", - "@tauri-apps/cli-linux-arm64-musl": "2.11.1", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.1", - "@tauri-apps/cli-linux-x64-gnu": "2.11.1", - "@tauri-apps/cli-linux-x64-musl": "2.11.1", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.1", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.1", - "@tauri-apps/cli-win32-x64-msvc": "2.11.1" + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", - "integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", "cpu": [ "arm64" ], @@ -4103,9 +4347,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz", - "integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", "cpu": [ "x64" ], @@ -4120,9 +4364,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz", - "integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", "cpu": [ "arm" ], @@ -4137,13 +4381,16 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz", - "integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -4154,13 +4401,16 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz", - "integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -4171,13 +4421,16 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz", - "integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -4188,13 +4441,16 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz", - "integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -4205,13 +4461,16 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz", - "integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -4222,9 +4481,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz", - "integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", "cpu": [ "arm64" ], @@ -4239,9 +4498,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz", - "integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", "cpu": [ "ia32" ], @@ -4256,9 +4515,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz", - "integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", "cpu": [ "x64" ], @@ -4281,6 +4540,16 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-opener/node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, "node_modules/@tauri-apps/plugin-process": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", @@ -4299,6 +4568,96 @@ "@tauri-apps/api": "^2.10.1" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -4350,6 +4709,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4395,6 +4762,17 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -4458,6 +4836,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4846,22 +5231,135 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn": { - "version": "8.16.0", + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, @@ -5010,6 +5508,26 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -5043,6 +5561,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -5214,6 +5742,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5509,6 +6047,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5660,6 +6219,20 @@ "node": ">= 12" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -5694,6 +6267,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -5785,6 +6365,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5811,6 +6401,14 @@ "node": ">=0.3.1" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -5902,6 +6500,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -5942,6 +6553,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -6241,6 +6859,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6317,6 +6945,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -6974,6 +7612,19 @@ "node": ">=16.9.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -7083,6 +7734,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -7263,6 +7924,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -7372,6 +8040,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7833,6 +8552,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -7881,6 +8611,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -7998,6 +8735,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -8239,6 +8986,17 @@ "node": ">= 10" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -8445,6 +9203,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8489,6 +9260,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8601,6 +9379,55 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -9067,6 +9894,20 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -9270,6 +10111,19 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -9519,6 +10373,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -9559,6 +10420,13 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -9569,6 +10437,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -9664,6 +10539,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -9690,6 +10578,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -9740,6 +10635,13 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", @@ -9767,6 +10669,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { "version": "7.0.30", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", @@ -9823,6 +10735,19 @@ "node": ">=16" } }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -9960,6 +10885,16 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -10227,6 +11162,109 @@ } } }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -10237,6 +11275,41 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10253,6 +11326,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -10350,6 +11440,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index aa0bdb4..3290687 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.3.0", "@tauri-apps/cli": "2.10.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/markdown-to-jsx": "^7.0.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -54,13 +57,15 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "jsdom": "^29.1.1", "prettier": "^3.8.3", "shadcn": "^3.8.5", "tailwindcss": "^4.3.0", "tw-animate-css": "^1.4.0", "typescript": "~5.8.3", "typescript-eslint": "^8.59.2", - "vite": "^7.3.3" + "vite": "^7.3.3", + "vitest": "^4.1.6" }, "overrides": { "ip-address": "^10.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec6da8d..9ddd8af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,13 +48,13 @@ importers: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: - specifier: ^19.2.6 + specifier: 19.2.6 version: 19.2.6 react-day-picker: specifier: ^9.14.0 version: 9.14.0(react@19.2.6) react-dom: - specifier: ^19.2.6 + specifier: 19.2.6 version: 19.2.6(react@19.2.6) react-hook-form: specifier: ^7.75.0 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 40dd63b..9ae965d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -247,6 +247,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -598,6 +613,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -610,14 +638,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.115", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" version = "0.21.3" @@ -687,6 +721,27 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.115", +] + [[package]] name = "digest" version = "0.10.7" @@ -715,7 +770,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -768,6 +823,21 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + [[package]] name = "dpi" version = "0.1.2" @@ -792,6 +862,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -875,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -963,6 +1048,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1418,7 +1509,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1459,10 +1550,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" version = "1.4.0" @@ -1877,10 +1978,10 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.13.0", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -1988,9 +2089,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -2528,10 +2640,20 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -2552,6 +2674,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -2582,6 +2714,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.10.0" @@ -2598,12 +2740,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.115", @@ -2636,6 +2778,15 @@ dependencies = [ "siphasher 1.0.2", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3055,6 +3206,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3074,7 +3231,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3130,7 +3287,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3141,9 +3298,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -3261,14 +3418,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", - "derive_more", + "cssparser 0.29.6", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] @@ -3440,6 +3616,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3574,6 +3759,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -3586,6 +3783,18 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3717,9 +3926,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -3785,9 +3994,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.5" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007" dependencies = [ "anyhow", "cargo_toml", @@ -3801,7 +4010,6 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.12+spec-1.1.0", "walkdir", ] @@ -3982,24 +4190,26 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http", "infer", "json-patch", "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf 0.13.1", + "plist", "proc-macro2", "quote", "regex", @@ -4039,7 +4249,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4053,6 +4263,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4705,6 +4925,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -4816,7 +5048,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5365,7 +5597,7 @@ dependencies = [ "dunce", "gdkx11", "gtk", - "html5ever", + "html5ever 0.29.1", "http", "javascriptcore-rs", "jni", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a347405..e8df4ee 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,12 +16,12 @@ name = "apex_circle_dasktop_application_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { version = "2.10", features = [] } +tauri-build = { version = "2.6.1", features = [] } [dependencies] -tauri = { version = "2.10", features = [] } -tauri-plugin-opener = "2.5" -tauri-plugin-process = "2.3" -tauri-plugin-updater = "2.10" +tauri = { version = "2.0", features = [] } +tauri-plugin-opener = "2.0" +tauri-plugin-process = "2.0" +tauri-plugin-updater = "2.0" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src/App.tsx b/src/App.tsx index 4738fbf..c39f7ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import { BrowserRouter } from "react-router-dom"; import "./App.css"; -import { dashboardData } from "./features/Member/v1/mock/dashboardData"; + import { startAutoUpdater } from "./system/updater/autoUpdater"; import { ThemeProvider } from "./theme"; @@ -16,7 +16,6 @@ function App() { void startAutoUpdater(); }, []); - const user = dashboardData.user; return ( diff --git a/src/Component/ui/Button.tsx b/src/Component/ui/Button.tsx index 5cd0949..ce6a5b7 100644 --- a/src/Component/ui/Button.tsx +++ b/src/Component/ui/Button.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useTheme } from "@/theme"; + type ButtonProps = { text: string; diff --git a/src/Component/ui/UniversalDatePicker.tsx b/src/Component/ui/UniversalDatePicker.tsx index 8a9455b..4a445e5 100644 --- a/src/Component/ui/UniversalDatePicker.tsx +++ b/src/Component/ui/UniversalDatePicker.tsx @@ -98,9 +98,9 @@ export const UniversalDatePicker = forwardRef diff --git a/src/features/Auth/v1/components/signup/ReviewStep.tsx b/src/features/Auth/v1/components/signup/ReviewStep.tsx index 12ba073..f7bb107 100644 --- a/src/features/Auth/v1/components/signup/ReviewStep.tsx +++ b/src/features/Auth/v1/components/signup/ReviewStep.tsx @@ -10,8 +10,7 @@ import { Github, Facebook, User, - Lock, - Image, + Lock } from "lucide-react"; import { SignupFormData } from "../../hooks/useSignupForm"; diff --git a/src/features/Auth/v1/hooks/useSignupForm.ts b/src/features/Auth/v1/hooks/useSignupForm.ts index 2dfcda5..b354eeb 100644 --- a/src/features/Auth/v1/hooks/useSignupForm.ts +++ b/src/features/Auth/v1/hooks/useSignupForm.ts @@ -86,7 +86,8 @@ export const signupSchema = z path: ["confirmPassword"], }); -export type SignupFormData = z.infer; +export type SignupFormInput = z.input; +export type SignupFormData = z.output; export const STEP_FIELDS: Record = { 1: ["communityName", "communityBio", "communityWebsite"], @@ -109,7 +110,7 @@ export const STEP_FIELDS: Record = { }; export function useSignupForm() { - return useForm({ + return useForm({ resolver: zodResolver(signupSchema), mode: "onTouched", defaultValues: { diff --git a/src/features/Events/v1/Components/Partners_And_Sponsors_Card.tsx b/src/features/Events/v1/Components/Partners_And_Sponsors_Card.tsx index 52f662d..8dd5b9f 100644 --- a/src/features/Events/v1/Components/Partners_And_Sponsors_Card.tsx +++ b/src/features/Events/v1/Components/Partners_And_Sponsors_Card.tsx @@ -1,6 +1,7 @@ -import { getTheme } from "@/config/them.config"; + import React, { memo } from "react"; import { CATEGORY_ACCENT_RULES, CategoryAccent, DEFAULT_ACCENT } from "../Constants/Event.constant"; +import { theme } from "@/theme"; type PartnersAndSponsorsCardProps = { image?: string; @@ -30,7 +31,7 @@ const Partners_And_Sponsors_Card = ({ name = "Partner/Sponsor Name", category = "Official Partner", }: PartnersAndSponsorsCardProps) => { - const theme = getTheme("light"); + const [hasImageError, setHasImageError] = React.useState(false); React.useEffect(() => { @@ -50,8 +51,8 @@ const Partners_And_Sponsors_Card = ({
diff --git a/src/features/Events/v1/Sections/Settings.tsx b/src/features/Events/v1/Sections/Settings.tsx index 6c2ad85..ba9facf 100644 --- a/src/features/Events/v1/Sections/Settings.tsx +++ b/src/features/Events/v1/Sections/Settings.tsx @@ -1,4 +1,4 @@ -import { getTheme } from "@/config/them.config"; + import { IoSettingsSharp } from "react-icons/io5"; import EventSetting from "../Components/EventSetting"; import { theme } from "@/theme"; diff --git a/src/features/Member/v1/mock/dashboardData.ts b/src/features/Member/v1/mock/dashboardData.ts index 9cbd8af..e0b988f 100644 --- a/src/features/Member/v1/mock/dashboardData.ts +++ b/src/features/Member/v1/mock/dashboardData.ts @@ -3,7 +3,7 @@ import { DashboardData } from "../../../Dashboard/Member/v1/Type/dashboard"; export const dashboardData: DashboardData = { user: { name: "Arjun Mehta", - role: "Member", + role: "Admin", }, summary: { diff --git a/src/features/SideBar/v1/Section/SideBar.tsx b/src/features/SideBar/v1/Section/SideBar.tsx index cd4ef5e..a80cf95 100644 --- a/src/features/SideBar/v1/Section/SideBar.tsx +++ b/src/features/SideBar/v1/Section/SideBar.tsx @@ -1,5 +1,5 @@ import { RiContactsBookFill } from "react-icons/ri"; -import { MdAssignment, MdDashboard, MdEvent, MdGroup, MdSettings, MdWork } from "react-icons/md"; +import { MdAssignment, MdDashboard, MdEvent, MdGroup, MdSettings, MdWork, MdWebhook } from "react-icons/md"; import { useTheme } from "@/theme"; import { ThemeToggle } from "@/Component/ui/ThemeToggle"; @@ -49,6 +49,7 @@ const SideBar = () => { } text="Teams" link="/org/member" /> } text="Events" link="/org/events" /> } text="Tasks" link="/org/tasks" /> + } text="Webhooks" link="/org/dashboard/webhooks" /> } text="Contact Submissions" link="/org/contact" /> {/* Footer */} diff --git a/src/features/Tasks/v1/components/common/ConfirmModal.tsx b/src/features/Tasks/v1/components/common/ConfirmModal.tsx index 25a0330..81e7dfe 100644 --- a/src/features/Tasks/v1/components/common/ConfirmModal.tsx +++ b/src/features/Tasks/v1/components/common/ConfirmModal.tsx @@ -1,5 +1,7 @@ import { useEffect, useRef } from "react"; -import { AlertTriangle, X, Loader2 } from "lucide-react"; +import { AlertTriangle, X, Loader2, CheckCircle2, Info, AlertCircle, LucideIcon } from "lucide-react"; + +type ModalVariant = "danger" | "success" | "warning" | "info"; interface ConfirmModalProps { isOpen: boolean; @@ -10,9 +12,37 @@ interface ConfirmModalProps { onConfirm: () => void; onCancel: () => void; isLoading?: boolean; - danger?: boolean; + variant?: ModalVariant; + icon?: LucideIcon; } +const VARIANT_CONFIG = { + danger: { + bg: "bg-[var(--cd-danger-subtle)]", + text: "text-[var(--cd-danger)]", + btn: "bg-[var(--cd-danger)] hover:bg-[var(--cd-danger-text)]", + icon: AlertTriangle, + }, + success: { + bg: "bg-[var(--cd-success-subtle)]", + text: "text-[var(--cd-success)]", + btn: "bg-[var(--cd-success)] hover:bg-[var(--cd-success-text)]", + icon: CheckCircle2, + }, + warning: { + bg: "bg-[var(--cd-warning-subtle)]", + text: "text-[var(--cd-warning)]", + btn: "bg-[var(--cd-warning)] hover:bg-[var(--cd-warning-text)]", + icon: AlertCircle, + }, + info: { + bg: "bg-[var(--cd-primary-subtle)]", + text: "text-[var(--cd-primary)]", + btn: "bg-[var(--cd-primary)] hover:bg-[var(--cd-primary-text)]", + icon: Info, + }, +}; + export default function ConfirmModal({ isOpen, title, @@ -22,11 +52,13 @@ export default function ConfirmModal({ onConfirm, onCancel, isLoading = false, - danger = false, + variant = "info", + icon: CustomIcon, }: ConfirmModalProps) { const cancelRef = useRef(null); + const config = VARIANT_CONFIG[variant]; + const Icon = CustomIcon || config.icon; - // Focus cancel on open & close on Escape useEffect(() => { if (!isOpen) return; cancelRef.current?.focus(); @@ -40,77 +72,58 @@ export default function ConfirmModal({ if (!isOpen) return null; return ( -
+
{/* Backdrop */}
!isLoading && onCancel()} /> {/* Dialog */}
- {/* Close */} -
- {/* Icon + Title */} -
-
- +
+
+
+
-
-

{title}

-

{message}

+ +
+

{title}

+

{message}

- {/* Actions */} -
+
-
); -} \ No newline at end of file +} diff --git a/src/features/Webhooks/v1/Webhook.types.ts b/src/features/Webhooks/v1/Webhook.types.ts new file mode 100644 index 0000000..2abf034 --- /dev/null +++ b/src/features/Webhooks/v1/Webhook.types.ts @@ -0,0 +1,75 @@ +export type WebhookEvent = + | "member.created" + | "member.activated" + | "event.created" + | "hackathon.created" + | "github.push" + | "github.pr.opened"; + +export type WebhookStatus = "active" | "inactive"; + +export interface Webhook { + id: string; + name: string; + url: string; + events: WebhookEvent[]; + status: WebhookStatus; + secret: string; // Typically masked like ********abcd + permissions?: string[]; + lastDeliveryStatus?: "success" | "failed" | "pending"; + lastTestedAt?: string; + lastTestStatus?: "success" | "failed"; + createdAt: string; + updatedAt: string; +} + +export interface WebhookLog { + id: string; + webhookId: string; + event: WebhookEvent; + status: "success" | "failed"; + timestamp: string; + responseCode: number; + requestPayload: unknown; + responsePayload: unknown; +} + +export interface CreateWebhookPayload { + name: string; + url: string; + events: WebhookEvent[]; + secret?: string; + permissions?: string[]; +} + +export interface UpdateWebhookPayload { + name?: string; + url?: string; + events?: WebhookEvent[]; + status?: WebhookStatus; + secret?: string; +} + +export interface WebhookFilters { + status: WebhookStatus | "all"; + search: string; + page: number; +} + +export interface PaginatedWebhooks { + data: Webhook[]; + total: number; + totalPages: number; +} + +export interface WebhookLogFilters { + status: "all" | "success" | "failed"; + event: WebhookEvent | "all"; + page: number; +} + +export interface PaginatedWebhookLogs { + data: WebhookLog[]; + total: number; + totalPages: number; +} diff --git a/src/features/Webhooks/v1/components/common/MaskedSecret.tsx b/src/features/Webhooks/v1/components/common/MaskedSecret.tsx new file mode 100644 index 0000000..572b620 --- /dev/null +++ b/src/features/Webhooks/v1/components/common/MaskedSecret.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { Eye, EyeOff, Copy, Check } from "lucide-react"; + +export default function MaskedSecret({ secret }: { secret: string }) { + const [show, setShow] = useState(false); + const [copied, setCopied] = useState(false); + + const displaySecret = show ? secret : secret.replace(/./g, "•"); + + const copyToClipboard = () => { + navigator.clipboard.writeText(secret); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ + {displaySecret} + +
+ + +
+
+ ); +} diff --git a/src/features/Webhooks/v1/components/common/StatusBadge.tsx b/src/features/Webhooks/v1/components/common/StatusBadge.tsx new file mode 100644 index 0000000..b69430b --- /dev/null +++ b/src/features/Webhooks/v1/components/common/StatusBadge.tsx @@ -0,0 +1,18 @@ +import { WebhookStatus } from "../../Webhook.types"; + +export default function StatusBadge({ status, className = "" }: { status: WebhookStatus; className?: string }) { + const isAct = status === "active"; + return ( + + + {isAct ? "Active" : "Inactive"} + + ); +} diff --git a/src/features/Webhooks/v1/components/form/WebhookForm.test.tsx b/src/features/Webhooks/v1/components/form/WebhookForm.test.tsx new file mode 100644 index 0000000..baf3e47 --- /dev/null +++ b/src/features/Webhooks/v1/components/form/WebhookForm.test.tsx @@ -0,0 +1,73 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import WebhookForm from "./WebhookForm"; +import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +// Mock the hooks +vi.mock("../../hooks/useWebhooks", () => ({ + useCreateWebhook: () => ({ mutateAsync: vi.fn().mockResolvedValue({}), isPending: false }), + useUpdateWebhook: () => ({ mutateAsync: vi.fn().mockResolvedValue({}), isPending: false }), +})); + +vi.mock("@/features/Tasks/v1/components/common/ToastNotification", () => ({ + useToast: () => ({ addToast: vi.fn() }), +})); + +const queryClient = new QueryClient(); + +const renderWithProviders = (ui: React.ReactElement) => { + return render( + + {ui} + + ); +}; + +describe("WebhookForm Component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders correctly in create mode", () => { + renderWithProviders(); + expect(screen.getByText("Endpoint Details")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("e.g. Production Slack Alerts")).toBeInTheDocument(); + }); + + it("validates empty form submission", async () => { + renderWithProviders(); + + const submitButton = screen.getByRole("button", { name: /Create Webhook/i }); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(screen.getByText("Name must be at least 2 characters")).toBeInTheDocument(); + expect(screen.getByText("Must be a valid URL")).toBeInTheDocument(); + expect(screen.getByText("Please select at least one event")).toBeInTheDocument(); + }); + }); + + it("validates HTTPS URL requirement", async () => { + renderWithProviders(); + + const urlInput = screen.getByPlaceholderText("https://your-domain.com/webhooks/commdesk"); + fireEvent.change(urlInput, { target: { value: "http://insecure-domain.com" } }); + + const submitButton = screen.getByRole("button", { name: /Create Webhook/i }); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(screen.getByText("URL must use HTTPS")).toBeInTheDocument(); + }); + }); + + it("allows selecting events", async () => { + renderWithProviders(); + + const eventOption = screen.getByText("Member Created"); + fireEvent.click(eventOption); + + expect(screen.getByText("1 Selected")).toBeInTheDocument(); + }); +}); diff --git a/src/features/Webhooks/v1/components/form/WebhookForm.tsx b/src/features/Webhooks/v1/components/form/WebhookForm.tsx new file mode 100644 index 0000000..be8a0eb --- /dev/null +++ b/src/features/Webhooks/v1/components/form/WebhookForm.tsx @@ -0,0 +1,299 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { useNavigate } from "react-router-dom"; +import { Check, Loader2, Link2, Shield, Eye, EyeOff, RefreshCw } from "lucide-react"; +import { WEBHOOK_EVENTS } from "../../constants/webhook.constants"; +import type { WebhookEvent, CreateWebhookPayload, UpdateWebhookPayload } from "../../Webhook.types"; +import { useCreateWebhook, useUpdateWebhook } from "../../hooks/useWebhooks"; +import { useToast } from "@/features/Tasks/v1/components/common/ToastNotification"; +import { Telemetry } from "@/utils/telemetry"; + +const webhookSchema = z.object({ + name: z.string().min(2, "Name must be at least 2 characters").max(50, "Name is too long"), + url: z.string().url("Must be a valid URL").refine(val => { + try { + const url = new URL(val); + const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1"; + if (isLocal) return true; + return val.startsWith("https://"); + } catch { + return false; + } + }, "URL must use HTTPS (except for localhost)"), + events: z.array(z.string()).min(1, "Please select at least one event"), + secret: z.string().optional(), + permissions: z.string().optional(), +}); + + +type FormData = z.infer; + +interface Props { + mode: "create" | "edit"; + initialData?: any; +} + +export default function WebhookForm({ mode, initialData }: Props) { + const navigate = useNavigate(); + const { addToast } = useToast(); + + const createWebhook = useCreateWebhook(); + const updateWebhook = useUpdateWebhook(); + const isSubmitting = createWebhook.isPending || updateWebhook.isPending; + + const [showSecret, setShowSecret] = useState(false); + + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(webhookSchema), + defaultValues: { + name: initialData?.name || "", + url: initialData?.url || "", + events: initialData?.events || [], + secret: "", // Masked initially in edit mode, empty string means unchanged + permissions: initialData?.permissions?.join(", ") || "", + }, + + }); + + const selectedEvents = watch("events"); + + const toggleEvent = (eventId: string) => { + Telemetry.trackAction("webhook_form_toggle_event", { eventId }); + const current = selectedEvents || []; + if (current.includes(eventId)) { + setValue("events", current.filter(id => id !== eventId), { shouldValidate: true }); + } else { + setValue("events", [...current, eventId], { shouldValidate: true }); + } + }; + + const handleRegenerateSecret = () => { + const randomSecret = Array.from(crypto.getRandomValues(new Uint8Array(24))) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + setValue("secret", randomSecret, { shouldDirty: true }); + setShowSecret(true); + Telemetry.trackAction("webhook_secret_regenerated"); + addToast("info", "Secret Generated", "A new secure secret has been generated."); + }; + + const onSubmit = async (data: FormData) => { + try { + const parsedPermissions = data.permissions + ? data.permissions.split(",").map(p => p.trim()).filter(Boolean) + : undefined; + + if (mode === "create") { + await createWebhook.mutateAsync({ + ...data, + permissions: parsedPermissions, + } as CreateWebhookPayload); + Telemetry.trackAction("webhook_created", { eventsCount: data.events.length }); + addToast("success", "Webhook created", "Your new webhook has been set up successfully."); + navigate("/org/dashboard/webhooks"); + } else { + const payload: UpdateWebhookPayload = { name: data.name, url: data.url, events: data.events as WebhookEvent[] }; + if (data.secret) payload.secret = data.secret; // only update if provided + if (parsedPermissions) payload.permissions = parsedPermissions; + await updateWebhook.mutateAsync({ id: initialData.id, payload }); + Telemetry.trackAction("webhook_updated", { id: initialData.id }); + addToast("success", "Webhook updated", "Changes saved successfully."); + navigate("/org/dashboard/webhooks"); + } + } catch (err) { + Telemetry.trackError("webhook_save_failed", err); + addToast("error", "Error saving webhook", "Please try again later."); + } + }; + + const onError = (errors: any) => { + Telemetry.trackFormError("WebhookForm", errors); + }; + + return ( +
+
+
+

+ {mode === "create" ? "Endpoint Details" : "Edit Endpoint"} +

+ + {/* Name Field */} +
+ + + {errors.name &&

{errors.name.message}

} +
+ + {/* URL Field */} +
+ +
+
+ +
+ +
+ {errors.url &&

{errors.url.message}

} +
+ + {/* Secret Field */} +
+ +
+
+ +
+ +
+ + +
+
+

+ Used to create a hash signature with each payload, ensuring the request comes from CommDesk. +

+
+ + {/* Permissions Field */} +
+ + +

+ Specific permissions required for this webhook endpoint. +

+
+ +
+ + {/* Events Multi-select */} +
+
+ + + {selectedEvents.length} Selected + +
+ {errors.events &&

{errors.events.message}

} + +
+ {WEBHOOK_EVENTS.map((event) => { + const isSelected = selectedEvents.includes(event.id); + return ( +
toggleEvent(event.id)} + className="flex items-start gap-3 p-3 rounded-xl border cursor-pointer transition-all hover:bg-[var(--cd-hover)]" + style={{ + backgroundColor: isSelected ? "var(--cd-primary-subtle)" : "var(--cd-surface-2)", + borderColor: isSelected ? "var(--cd-primary)" : "var(--cd-border)", + }} + > +
+
+ {isSelected && } +
+
+
+

+ {event.label} +

+

+ {event.description} +

+
+
+ ); + })} +
+
+
+
+ + {/* Floating Action Bar */} +
+
+
+

{mode === "create" ? "New Webhook" : "Edit Webhook"}

+

{watch("name") || "Untitled Webhook"}

+
+
+ + +
+
+
+
+ ); +} + + diff --git a/src/features/Webhooks/v1/components/layout/BulkActionBar.tsx b/src/features/Webhooks/v1/components/layout/BulkActionBar.tsx new file mode 100644 index 0000000..fbc6adf --- /dev/null +++ b/src/features/Webhooks/v1/components/layout/BulkActionBar.tsx @@ -0,0 +1,59 @@ +import { Trash2, ShieldCheck, ShieldAlert, X } from "lucide-react"; + +interface Props { + selectedCount: number; + onClear: () => void; + onAction: (action: "delete" | "enable" | "disable") => void; +} + +export default function BulkActionBar({ selectedCount, onClear, onAction }: Props) { + if (selectedCount === 0) return null; + + return ( +
+
+ + + {selectedCount} Selected + +
+ +
+ + + +
+
+ ); +} diff --git a/src/features/Webhooks/v1/components/layout/WebhookFilters.tsx b/src/features/Webhooks/v1/components/layout/WebhookFilters.tsx new file mode 100644 index 0000000..8685744 --- /dev/null +++ b/src/features/Webhooks/v1/components/layout/WebhookFilters.tsx @@ -0,0 +1,228 @@ +import { useEffect, useRef, useState, useLayoutEffect } from "react"; +import { createPortal } from "react-dom"; +import { Search, X, SlidersHorizontal, ChevronDown, Check } from "lucide-react"; +import type { WebhookFilters, WebhookStatus } from "../../Webhook.types"; + +interface Props { + filters: WebhookFilters; + onChange: (f: WebhookFilters) => void; + filteredCount: number; + totalCount: number; +} + +const STATUS_DOTS: Record = { all: "bg-gray-400", active: "bg-emerald-500", inactive: "bg-gray-500" }; + +const STATUS_STYLES: Record = { + active: { bg: "var(--cd-success-subtle)", color: "var(--cd-success)", border: "var(--cd-success-subtle)" }, + inactive: { bg: "var(--cd-surface-3)", color: "var(--cd-text-2)", border: "var(--cd-border)" }, +}; + +function useDropdown() { + const [open, setOpen] = useState(false); + const btnRef = useRef(null); + const panelRef = useRef(null); + useEffect(() => { + if (!open) return; + const h = (e: MouseEvent) => { + const t = e.target as Node; + if (!btnRef.current?.contains(t) && !panelRef.current?.contains(t)) setOpen(false); + }; + document.addEventListener("mousedown", h); + return () => document.removeEventListener("mousedown", h); + }, [open]); + return { open, setOpen, btnRef, panelRef }; +} + +function DropdownPortal({ btnRef, panelRef, open, children }: { + btnRef: React.RefObject; + panelRef: React.RefObject; + open: boolean; + children: React.ReactNode; +}) { + const [style, setStyle] = useState({}); + useLayoutEffect(() => { + if (!open || !btnRef.current) return; + const rect = btnRef.current.getBoundingClientRect(); + const below = window.innerHeight - rect.bottom; + if (below < 240 && rect.top > below) + setStyle({ position: "fixed", left: rect.left, bottom: window.innerHeight - rect.top + 6, zIndex: 9999 }); + else + setStyle({ position: "fixed", left: rect.left, top: rect.bottom + 6, zIndex: 9999 }); + }, [open, btnRef]); + if (!open) return null; + return createPortal( +
+ {children} +
, + document.body + ); +} + +export function PillDropdown({ label: pillLabel, value, options, onChange, dotMap, styleMap }: { + label: string; + value: T; + options: { value: T; label: string }[]; + onChange: (v: T) => void; + dotMap: Record; + styleMap: Record; +}) { + const { open, setOpen, btnRef, panelRef } = useDropdown(); + const isActive = value !== options[0].value; + const current = options.find((o) => o.value === value)?.label ?? pillLabel; + const activeStyle = styleMap[value] ?? { bg: "var(--cd-primary)", color: "#fff", border: "var(--cd-primary)" }; + + return ( +
+ + + + {options.map((opt) => ( + + ))} + +
+ ); +} + +export default function WebhookFiltersBar({ filters, onChange, filteredCount, totalCount }: Props) { + const [localSearch, setLocalSearch] = useState(filters.search); + const onChangeRef = useRef(onChange); + + useEffect(() => { onChangeRef.current = onChange; }, [onChange]); + + useEffect(() => { + if (localSearch === filters.search) return; + const timer = setTimeout(() => { + onChangeRef.current({ ...filters, search: localSearch }); + }, 300); + return () => clearTimeout(timer); + }, [localSearch, filters]); + + const hasActive = filters.status !== "all" || filters.search !== ""; + + const resetAll = () => { + setLocalSearch(""); + onChange({ status: "all", search: "" }); + }; + + return ( +
+
+ + + {/* Status Dropdown */} + + label="Status" + value={filters.status} + dotMap={STATUS_DOTS} + styleMap={STATUS_STYLES} + onChange={(v) => onChange({ ...filters, status: v })} + options={[ + { value: "all", label: "All Status" }, + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, + ]} + /> + + {/* Search */} +
+
+ + setLocalSearch(e.target.value)} + placeholder="Search webhooks by name or URL..." + className="flex-1 text-xs bg-transparent outline-none min-w-0" + style={{ color: "var(--cd-text)" }} + /> + {localSearch && ( + + )} +
+
+ + {hasActive && ( + + )} +
+ +
+

+ {hasActive ? ( + + + Showing {filteredCount} results + + ) : ( + `Total webhooks: ${totalCount}` + )} +

+
+
+ ); +} diff --git a/src/features/Webhooks/v1/components/layout/WebhookHeader.tsx b/src/features/Webhooks/v1/components/layout/WebhookHeader.tsx new file mode 100644 index 0000000..006492c --- /dev/null +++ b/src/features/Webhooks/v1/components/layout/WebhookHeader.tsx @@ -0,0 +1,51 @@ +import { useNavigate } from "react-router-dom"; +import { Plus, Webhook as WebhookIcon } from "lucide-react"; + +interface Props { + totalCount: number; + activeCount: number; +} + +export default function WebhookHeader({ totalCount, activeCount }: Props) { + const navigate = useNavigate(); + + return ( +
+
+
+
+ +
+
+

+ Webhooks +

+

+ Manage integrations and real-time events ({activeCount} active of {totalCount}) +

+
+
+ + +
+
+ ); +} diff --git a/src/features/Webhooks/v1/components/modals/PayloadModal.tsx b/src/features/Webhooks/v1/components/modals/PayloadModal.tsx new file mode 100644 index 0000000..59f2c6e --- /dev/null +++ b/src/features/Webhooks/v1/components/modals/PayloadModal.tsx @@ -0,0 +1,54 @@ +import { X } from "lucide-react"; +import { useEffect } from "react"; + +interface Props { + isOpen: boolean; + onClose: () => void; + title: string; + payload: any; +} + +export default function PayloadModal({ isOpen, onClose, title, payload }: Props) { + // Prevent body scrolling when modal is open + useEffect(() => { + if (isOpen) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [isOpen]); + + if (!isOpen) return null; + + return ( +
+
+
+

{title}

+ +
+ +
+
+            {JSON.stringify(payload, null, 2)}
+          
+
+
+
+ ); +} diff --git a/src/features/Webhooks/v1/components/table/LogsCardList.tsx b/src/features/Webhooks/v1/components/table/LogsCardList.tsx new file mode 100644 index 0000000..27af837 --- /dev/null +++ b/src/features/Webhooks/v1/components/table/LogsCardList.tsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import { CheckCircle2, XCircle, RotateCcw, FileJson, Clock } from "lucide-react"; +import { format } from "date-fns"; +import type { WebhookLog } from "../../Webhook.types"; +import PayloadModal from "../modals/PayloadModal"; + +interface Props { + logs: WebhookLog[]; + isLoading: boolean; + onRetry: (logId: string) => void; + isRetrying: boolean; +} + +export default function LogsCardList({ logs, isLoading, onRetry, isRetrying }: Props) { + const [selectedPayload, setSelectedPayload] = useState<{ title: string; data: any } | null>(null); + + if (isLoading || logs.length === 0) return null; + + return ( + <> +
+ {logs.map((log) => { + const isSuccess = log.status === "success"; + return ( +
+
+
+ {isSuccess ? ( + + ) : ( + + )} + + {log.status} + +
+
= 200 && log.responseCode < 300 ? "var(--cd-success-subtle)" : "var(--cd-danger-subtle)", + color: log.responseCode >= 200 && log.responseCode < 300 ? "var(--cd-success)" : "var(--cd-danger)" + }} + > + {log.responseCode} +
+
+ +
+
+
+ +
+
+

Delivery Time

+

+ {format(new Date(log.timestamp), "MMM d, HH:mm:ss")} +

+
+
+ +
+
+
EV
+
+
+

Trigger Event

+

+ {log.event} +

+
+
+
+ +
+ + + {!isSuccess && ( + + )} +
+
+ ); + })} +
+ + setSelectedPayload(null)} + title={selectedPayload?.title || ""} + payload={selectedPayload?.data} + /> + + ); +} diff --git a/src/features/Webhooks/v1/components/table/LogsTable.tsx b/src/features/Webhooks/v1/components/table/LogsTable.tsx new file mode 100644 index 0000000..8788648 --- /dev/null +++ b/src/features/Webhooks/v1/components/table/LogsTable.tsx @@ -0,0 +1,119 @@ +import { useState } from "react"; +import { CheckCircle2, XCircle, RotateCcw, FileJson } from "lucide-react"; +import { format } from "date-fns"; +import type { WebhookLog } from "../../Webhook.types"; +import PayloadModal from "../modals/PayloadModal"; + +interface Props { + logs: WebhookLog[]; + isLoading: boolean; + onRetry: (logId: string) => void; + isRetrying: boolean; +} + +export default function LogsTable({ logs, isLoading, onRetry, isRetrying }: Props) { + const [selectedPayload, setSelectedPayload] = useState<{ title: string; data: any } | null>(null); + + if (isLoading) { + return
Loading delivery logs...
; + } + + if (logs.length === 0) { + return ( +
+

No deliveries found.

+

Deliveries will appear here when an event triggers this webhook.

+
+ ); + } + + return ( + <> +
+ + + + + + + + + + + + {logs.map((log) => { + const isSuccess = log.status === "success"; + return ( + + + + + + + + ); + })} + +
StatusTimestampEventCodePayloads
+
+
+ + {log.status} + +
+
+ {format(new Date(log.timestamp), "MMM d, HH:mm:ss")} + + + {log.event} + + + + {log.responseCode} + + +
+ + + {!isSuccess && ( + + )} +
+
+
+ + setSelectedPayload(null)} + title={selectedPayload?.title || ""} + payload={selectedPayload?.data} + /> + + ); +} diff --git a/src/features/Webhooks/v1/components/table/WebhookCardList.tsx b/src/features/Webhooks/v1/components/table/WebhookCardList.tsx new file mode 100644 index 0000000..f32442d --- /dev/null +++ b/src/features/Webhooks/v1/components/table/WebhookCardList.tsx @@ -0,0 +1,113 @@ +import { Edit, Trash2, Webhook as WebhookIcon, Power } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import type { Webhook } from "../../Webhook.types"; +import StatusBadge from "../common/StatusBadge"; + +interface Props { + webhooks: Webhook[]; + isLoading: boolean; + onDelete: (w: Webhook) => void; + onToggleStatus: (w: Webhook) => void; + selectedIds: string[]; + onToggleSelect: (id: string) => void; +} + +export default function WebhookCardList({ + webhooks, + isLoading, + onDelete, + onToggleStatus, + selectedIds, + onToggleSelect +}: Props) { + const navigate = useNavigate(); + + if (isLoading) { + return ( +
+ Loading webhooks... +
+ ); + } + + if (webhooks.length === 0) { + return null; + } + + return ( +
+ {webhooks.map((w) => ( +
+
+ onToggleSelect(w.id)} + className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer accent-indigo-600" + /> +
+
+
navigate(`/org/dashboard/webhooks/${w.id}`)}> +
+ +
+
+

{w.name}

+

+ {w.url.replace(/^https?:\/\//, '')} +

+
+
+
+ +
+ + + {w.events.length} Events + +
+ +
+
+ Last: + + {w.lastDeliveryStatus ? w.lastDeliveryStatus.charAt(0).toUpperCase() + w.lastDeliveryStatus.slice(1) : "None"} + +
+ +
+ + + +
+
+
+ ))} +
+ ); +} diff --git a/src/features/Webhooks/v1/components/table/WebhookTable.tsx b/src/features/Webhooks/v1/components/table/WebhookTable.tsx new file mode 100644 index 0000000..b60e358 --- /dev/null +++ b/src/features/Webhooks/v1/components/table/WebhookTable.tsx @@ -0,0 +1,179 @@ +import { MoreHorizontal, Edit, Trash2, Webhook as WebhookIcon, Settings } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import type { Webhook } from "../../Webhook.types"; +import StatusBadge from "../common/StatusBadge"; +import { format } from "date-fns"; + +interface Props { + webhooks: Webhook[]; + isLoading: boolean; + onDelete: (w: Webhook) => void; + onToggleStatus: (w: Webhook) => void; + selectedIds: string[]; + onToggleSelect: (id: string) => void; + onSelectAll: (ids: string[]) => void; +} + +export default function WebhookTable({ + webhooks, + isLoading, + onDelete, + onToggleStatus, + selectedIds, + onToggleSelect, + onSelectAll +}: Props) { + const navigate = useNavigate(); + + const allSelected = webhooks.length > 0 && selectedIds.length === webhooks.length; + + if (isLoading) { + return ( +
+ Loading webhooks... +
+ ); + } + + if (webhooks.length === 0) { + return null; // Handled by EmptyState in the parent + } + + return ( +
+ + + + + + + + + + + + + {webhooks.map((w) => { + const isSuccess = w.lastDeliveryStatus === "success"; + const isFailed = w.lastDeliveryStatus === "failed"; + + return ( + + + + + + + + + + + + + ); + })} + +
+ onSelectAll(allSelected ? [] : webhooks.map(w => w.id))} + className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer accent-indigo-600" + /> + NameURLStatusLast DeliveryActions
+ onToggleSelect(w.id)} + className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer accent-indigo-600" + /> + +
navigate(`/org/dashboard/webhooks/${w.id}`)}> +
+ +
+
+

{w.name}

+

+ {w.events.length} event{w.events.length !== 1 ? "s" : ""} +

+
+
+
+
+

+ {w.url.replace(/^https?:\/\//, '')} +

+
+
+ + +
+ {w.lastDeliveryStatus ? ( + <> +
+ + {w.lastDeliveryStatus} + + + ) : ( + Never + )} +
+
+
+ + + +
+ {/* Mobile fallback for ellipsis menu if needed, but opacity-0 works fine for desktop hover */} +
+
+ ); +} diff --git a/src/features/Webhooks/v1/constants/webhook.constants.ts b/src/features/Webhooks/v1/constants/webhook.constants.ts new file mode 100644 index 0000000..77cfaa9 --- /dev/null +++ b/src/features/Webhooks/v1/constants/webhook.constants.ts @@ -0,0 +1,22 @@ +import { WebhookEvent, WebhookFilters, WebhookLogFilters } from "../Webhook.types"; + +export const WEBHOOK_EVENTS: { id: WebhookEvent; label: string; description: string }[] = [ + { id: "member.created", label: "Member Created", description: "Triggered when a new member joins." }, + { id: "member.activated", label: "Member Activated", description: "Triggered when a member account is activated." }, + { id: "event.created", label: "Event Created", description: "Triggered when a new event is scheduled." }, + { id: "hackathon.created", label: "Hackathon Created", description: "Triggered when a new hackathon is created." }, + { id: "github.push", label: "GitHub Push", description: "Triggered on a repository push event." }, + { id: "github.pr.opened", label: "GitHub PR Opened", description: "Triggered when a pull request is opened." }, +]; + +export const DEFAULT_WEBHOOK_FILTERS: WebhookFilters = { + status: "all", + search: "", + page: 1, +}; + +export const DEFAULT_LOG_FILTERS: WebhookLogFilters = { + status: "all", + event: "all", + page: 1, +}; diff --git a/src/features/Webhooks/v1/hooks/useWebhookLogs.ts b/src/features/Webhooks/v1/hooks/useWebhookLogs.ts new file mode 100644 index 0000000..118db50 --- /dev/null +++ b/src/features/Webhooks/v1/hooks/useWebhookLogs.ts @@ -0,0 +1,52 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { webhookLogStore } from "../mock/webhookStore"; +import type { WebhookLog, WebhookLogFilters, PaginatedWebhookLogs } from "../Webhook.types"; + +export function useWebhookLogs(webhookId: string | undefined, filters: WebhookLogFilters) { + return useQuery({ + queryKey: ["webhook-logs", webhookId, filters], + queryFn: async () => { + await new Promise((r) => setTimeout(r, 600)); + if (!webhookId) return { data: [], total: 0, totalPages: 0 }; + + let logs = webhookLogStore.getByWebhookId(webhookId); + + if (filters.status !== "all") { + logs = logs.filter(l => l.status === filters.status); + } + if (filters.event !== "all") { + logs = logs.filter(l => l.event === filters.event); + } + + const pageSize = 10; + const total = logs.length; + const totalPages = Math.ceil(total / pageSize); + const page = filters.page || 1; + const start = (page - 1) * pageSize; + const paginatedLogs = logs.slice(start, start + pageSize); + + return { + data: paginatedLogs, + total, + totalPages, + }; + }, + enabled: !!webhookId, + }); +} + +export function useRetryWebhookDelivery() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ webhookId, logId }: { webhookId: string; logId: string }): Promise<{ success: boolean }> => { + await new Promise((r) => setTimeout(r, 800)); + // Mock successful retry + return { success: true }; + }, + onSuccess: (_, { webhookId }) => { + qc.invalidateQueries({ queryKey: ["webhook-logs", webhookId] }); + qc.invalidateQueries({ queryKey: ["webhooks", webhookId] }); + qc.invalidateQueries({ queryKey: ["webhooks"] }); + }, + }); +} diff --git a/src/features/Webhooks/v1/hooks/useWebhooks.test.tsx b/src/features/Webhooks/v1/hooks/useWebhooks.test.tsx new file mode 100644 index 0000000..18db5eb --- /dev/null +++ b/src/features/Webhooks/v1/hooks/useWebhooks.test.tsx @@ -0,0 +1,60 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, it, expect, beforeEach } from "vitest"; +import { useWebhooks, useWebhook, useCreateWebhook } from "./useWebhooks"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { webhookStore } from "../mock/webhookStore"; + +describe("Webhook API Hooks Integration", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient(); + // Reset mock store before each test + const all = webhookStore.getAll(); + all.forEach(w => webhookStore.remove(w.id)); + }); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + it("creates a new webhook successfully", async () => { + const { result } = renderHook(() => useCreateWebhook(), { wrapper }); + + let newWebhook: any; + await result.current.mutateAsync({ + name: "Test Hook", + url: "https://test.com/hook", + events: ["member.created"], + }).then(res => newWebhook = res); + + expect(newWebhook).toBeDefined(); + expect(newWebhook.name).toBe("Test Hook"); + expect(newWebhook.url).toBe("https://test.com/hook"); + expect(newWebhook.status).toBe("active"); + }); + + it("fetches webhooks with filters", async () => { + webhookStore.add({ + id: "test-1", + name: "Alpha Hook", + url: "https://alpha.com", + events: ["event.created"], + status: "active", + secret: "sec", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + const { result } = renderHook(() => useWebhooks({ status: "all", search: "Alpha" }), { wrapper }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toHaveLength(1); + expect(result.current.data?.[0].name).toBe("Alpha Hook"); + }); +}); diff --git a/src/features/Webhooks/v1/hooks/useWebhooks.ts b/src/features/Webhooks/v1/hooks/useWebhooks.ts new file mode 100644 index 0000000..9d7f8db --- /dev/null +++ b/src/features/Webhooks/v1/hooks/useWebhooks.ts @@ -0,0 +1,160 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { webhookStore } from "../mock/webhookStore"; +import type { Webhook, WebhookFilters, CreateWebhookPayload, UpdateWebhookPayload, PaginatedWebhooks } from "../Webhook.types"; +import { Telemetry } from "@/utils/telemetry"; + +function applyFilters(webhooks: Webhook[], filters: WebhookFilters): Webhook[] { + return webhooks.filter((w) => { + if (filters.status !== "all" && w.status !== filters.status) return false; + if (filters.search) { + const query = filters.search.toLowerCase(); + const match = w.name.toLowerCase().includes(query) || w.url.toLowerCase().includes(query); + if (!match) return false; + } + return true; + }); +} + +export function useWebhooks(filters: WebhookFilters) { + return useQuery({ + queryKey: ["webhooks", filters], + queryFn: async () => { + await new Promise((r) => setTimeout(r, 600)); // Simulate latency + const allWebhooks = webhookStore.getAll(); + const filtered = applyFilters(allWebhooks, filters); + + const pageSize = 10; + const total = filtered.length; + const totalPages = Math.ceil(total / pageSize); + const page = filters.page || 1; + const start = (page - 1) * pageSize; + const paginatedData = filtered.slice(start, start + pageSize); + + return { + data: paginatedData, + total, + totalPages, + }; + }, + }); +} + +export function useWebhook(id: string | undefined) { + return useQuery({ + queryKey: ["webhooks", id], + queryFn: async () => { + await new Promise((r) => setTimeout(r, 400)); + if (!id) return undefined; + return webhookStore.getById(id); + }, + enabled: !!id, + }); +} + +export function useCreateWebhook() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (payload: CreateWebhookPayload): Promise => { + await new Promise((r) => setTimeout(r, 500)); + const newWebhook: Webhook = { + id: `wh-${Date.now()}`, + name: payload.name, + url: payload.url, + events: payload.events, + status: "active", + secret: payload.secret || Array.from(crypto.getRandomValues(new Uint8Array(24))).map(b => b.toString(16).padStart(2, '0')).join(''), + permissions: payload.permissions, + lastDeliveryStatus: "pending", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + webhookStore.add(newWebhook); + return newWebhook; + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["webhooks"] }), + }); +} + +export function useUpdateWebhook() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, payload }: { id: string; payload: UpdateWebhookPayload }): Promise => { + await new Promise((r) => setTimeout(r, 400)); + webhookStore.update(id, payload); + const updated = webhookStore.getById(id); + if (!updated) { + Telemetry.trackError("webhook_update_not_found", { id }); + throw new Error("Webhook not found"); + } + return updated; + }, + onSuccess: (_, { id }) => { + qc.invalidateQueries({ queryKey: ["webhooks"] }); + qc.invalidateQueries({ queryKey: ["webhooks", id] }); + }, + }); +} + +export function useDeleteWebhook() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id: string): Promise => { + await new Promise((r) => setTimeout(r, 300)); + webhookStore.remove(id); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["webhooks"] }), + }); +} + +export function useTestWebhook() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id: string): Promise<{ success: boolean; message: string }> => { + await new Promise((r) => setTimeout(r, 3500)); // Simulated 3.5s delay + const webhook = webhookStore.getById(id); + if (!webhook) { + Telemetry.trackError("webhook_test_not_found", { id }); + throw new Error("Webhook not found"); + } + + // 80% chance of success for mock + const isSuccess = Math.random() > 0.2; + + webhookStore.update(id, { + lastTestedAt: new Date().toISOString(), + lastTestStatus: isSuccess ? "success" : "failed" + }); + + Telemetry.trackAction("webhook_tested", { id, isSuccess }); + return { + success: isSuccess, + message: isSuccess ? "Webhook ping successful" : "Failed to reach endpoint" + }; + }, + onSuccess: (_, id) => { + qc.invalidateQueries({ queryKey: ["webhooks"] }); + qc.invalidateQueries({ queryKey: ["webhooks", id] }); + }, + }); +} +export function useBulkWebhookAction() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ ids, action }: { ids: string[]; action: "delete" | "enable" | "disable" }): Promise => { + await new Promise((r) => setTimeout(r, 600)); // Simulated delay + + ids.forEach((id) => { + if (action === "delete") { + webhookStore.remove(id); + } else { + webhookStore.update(id, { status: action === "enable" ? "active" : "inactive" }); + } + }); + + Telemetry.trackAction("webhooks_bulk_action", { action, count: ids.length }); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["webhooks"] }); + }, + }); +} diff --git a/src/features/Webhooks/v1/mock/webhookStore.ts b/src/features/Webhooks/v1/mock/webhookStore.ts new file mode 100644 index 0000000..67fe168 --- /dev/null +++ b/src/features/Webhooks/v1/mock/webhookStore.ts @@ -0,0 +1,67 @@ +import type { Webhook, WebhookLog } from "../Webhook.types"; + +let mockWebhooks: Webhook[] = [ + { + id: "wh-1", + name: "Slack Notifications", + url: "https://hooks.slack.com/services/REPLACE_WITH_YOUR_ACTUAL_WEBHOOK_URL", + events: ["member.created", "event.created"], + status: "active", + secret: "432d56a1b2c3d4e5f6g7h8i9j0k1l2m3", + lastDeliveryStatus: "success", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "wh-2", + name: "Zapier Integration", + url: "https://hooks.zapier.com/hooks/catch/123456/abcdef/", + events: ["hackathon.created"], + status: "inactive", + secret: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + lastDeliveryStatus: "pending", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } +]; + +let mockLogs: WebhookLog[] = [ + { + id: "log-1", + webhookId: "wh-1", + event: "member.created", + status: "success", + timestamp: new Date().toISOString(), + responseCode: 200, + requestPayload: { memberId: "m-1", name: "John Doe" }, + responsePayload: { success: true }, + }, + { + id: "log-2", + webhookId: "wh-1", + event: "event.created", + status: "failed", + timestamp: new Date(Date.now() - 3600000).toISOString(), + responseCode: 500, + requestPayload: { eventId: "e-1", title: "Tech Meetup" }, + responsePayload: { error: "Internal Server Error" }, + } +]; + +export const webhookStore = { + getAll: () => [...mockWebhooks], + getById: (id: string) => mockWebhooks.find((w) => w.id === id), + add: (webhook: Webhook) => { + mockWebhooks.push(webhook); + }, + update: (id: string, updates: Partial) => { + mockWebhooks = mockWebhooks.map((w) => (w.id === id ? { ...w, ...updates, updatedAt: new Date().toISOString() } : w)); + }, + remove: (id: string) => { + mockWebhooks = mockWebhooks.filter((w) => w.id !== id); + }, +}; + +export const webhookLogStore = { + getByWebhookId: (webhookId: string) => mockLogs.filter((l) => l.webhookId === webhookId), +}; diff --git a/src/features/Webhooks/v1/pages/CreateWebhookPage.tsx b/src/features/Webhooks/v1/pages/CreateWebhookPage.tsx new file mode 100644 index 0000000..8e4c07c --- /dev/null +++ b/src/features/Webhooks/v1/pages/CreateWebhookPage.tsx @@ -0,0 +1,51 @@ +import { useNavigate } from "react-router-dom"; +import { ArrowLeft, Plus } from "lucide-react"; +import WebhookForm from "../components/form/WebhookForm"; +import { ToastContainer } from "@/features/Tasks/v1/components/common/ToastNotification"; + +export default function CreateWebhookPage() { + const navigate = useNavigate(); + + return ( +
+ {/* Header */} +
+
+

+ +
+ +
+ Create Webhook +

+
+
+ + {/* Form content */} +
+ +
+ + {}} /> +
+ ); +} diff --git a/src/features/Webhooks/v1/pages/EditWebhookPage.tsx b/src/features/Webhooks/v1/pages/EditWebhookPage.tsx new file mode 100644 index 0000000..f2d6a3e --- /dev/null +++ b/src/features/Webhooks/v1/pages/EditWebhookPage.tsx @@ -0,0 +1,63 @@ +import { useNavigate, useParams } from "react-router-dom"; +import { ArrowLeft, Edit } from "lucide-react"; +import WebhookForm from "../components/form/WebhookForm"; +import { useWebhook } from "../hooks/useWebhooks"; +import { ToastContainer } from "@/features/Tasks/v1/components/common/ToastNotification"; + +export default function EditWebhookPage() { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + + const { data: webhook, isLoading, isError } = useWebhook(id); + + if (isLoading) { + return
Loading webhook details...
; + } + + if (isError || !webhook) { + return
Webhook not found or failed to load.
; + } + + return ( +
+ {/* Header */} +
+
+

+ +
+ +
+ Edit Webhook +

+
+
+ + {/* Form content */} +
+ +
+ + {}} /> +
+ ); +} diff --git a/src/features/Webhooks/v1/pages/WebhookDetailsPage.tsx b/src/features/Webhooks/v1/pages/WebhookDetailsPage.tsx new file mode 100644 index 0000000..2428158 --- /dev/null +++ b/src/features/Webhooks/v1/pages/WebhookDetailsPage.tsx @@ -0,0 +1,256 @@ +import { useState } from "react"; +import { useNavigate, useParams, Link } from "react-router-dom"; +import { ArrowLeft, Activity, ShieldAlert, CheckCircle2, Settings2, TestTube2, Loader2, ArrowRight, Signal, Zap, Clock, Terminal, Globe, Code2, AlertCircle, Copy, Check } from "lucide-react"; +import { useWebhook, useTestWebhook } from "../hooks/useWebhooks"; +import { useWebhookLogs } from "../hooks/useWebhookLogs"; +import StatusBadge from "../components/common/StatusBadge"; +import MaskedSecret from "../components/common/MaskedSecret"; +import LogsTable from "../components/table/LogsTable"; +import { ToastContainer, useToast } from "@/features/Tasks/v1/components/common/ToastNotification"; +import { format } from "date-fns"; + +export default function WebhookDetailsPage() { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const { toasts, addToast, dismiss } = useToast(); + const [testResult, setTestResult] = useState<{ success: boolean; message: string; timestamp: Date } | null>(null); + const [copied, setCopied] = useState(false); + + const { data: webhook, isLoading, isError } = useWebhook(id); + const { data: logsData, isLoading: logsLoading } = useWebhookLogs(id, { page: 1, status: "all", event: "all" }); + const testWebhook = useTestWebhook(); + + if (isLoading) return
Loading webhook...
; + if (isError || !webhook) return
Webhook not found.
; + + const handleCopy = () => { + navigator.clipboard.writeText(webhook.url); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + addToast("success", "URL Copied", "Webhook URL added to clipboard"); + }; + + const handleTest = async () => { + try { + const res = await testWebhook.mutateAsync(webhook.id); + setTestResult({ success: res.success, message: res.message, timestamp: new Date() }); + if (res.success) { + addToast("success", "Ping Sent", "Endpoint returned 200 OK"); + } else { + addToast("error", "Test Failed", res.message); + } + } catch { + setTestResult({ success: false, message: "Unable to reach endpoint. Check your URL.", timestamp: new Date() }); + addToast("error", "Test Failed", "Connection timeout."); + } + }; + + return ( +
+ {/* Header */} +
+
+ +
+

+ {webhook.name} +

+
+
+ {webhook.url} +
+ +
+
+
+ +
+ + +
+
+ +
+
+ + {/* Unified Dash Stats */} +
+ {[ + { label: "Reliability", val: "99.8%", icon: Zap, color: "var(--cd-success)" }, + { label: "Avg Latency", val: "142ms", icon: Clock, color: "var(--cd-primary)" }, + { label: "Volume (24h)", val: "1.2k", icon: Activity, color: "var(--cd-text)" }, + { label: "Status", val: webhook.status, icon: Signal, color: webhook.status === 'active' ? "var(--cd-success)" : "var(--cd-text-muted)" }, + ].map((stat, i) => ( +
+ {stat.label} +
+ + {stat.val} +
+
+ ))} +
+ + {/* Test Result Banner (Conditional) */} + {testResult && ( +
+
+
+ {testResult.success ? : } +
+
+

+ {testResult.success ? "Connection Successful" : "Connection Failed"} +

+

+ {testResult.message} • {format(testResult.timestamp, "HH:mm:ss")} +

+
+
+ +
+ )} + +
+ + {/* Left Column: Activity & Docs */} +
+ + {/* Recent Deliveries */} +
+
+
+
+ +
+

Recent Deliveries

+
+ Full History +
+
+ {logsLoading ? ( +
+ Loading Activity... +
+ ) : ( + {}} /> + )} +
+
{/* Payload Example */} +
+
+ +

Payload Structure

+
+
+
+
JSON
+
+                      {"{"}{"\n"}
+                      {"  "}"id": "evt_12345",{"\n"}
+                      {"  "}"type": "{webhook.events[0] || "member.created"}",{"\n"}
+                      {"  "}"created": {Date.now()},{"\n"}
+                      {"  "}"data": {"{"}{"\n"}
+                      {"    "}"object": "member",{"\n"}
+                      {"    "}"id": "mem_98765",{"\n"}
+                      {"    "}"status": "active"{"\n"}
+                      {"  "}{"}"}{"\n"}
+                      {"}"}
+                    
+
+

+ Note: Requests are POSTed with an X-CommDesk-Signature header for verification. +

+
+
+ +
+ + {/* Right Column: Configuration */} +
+ + {/* Security Sidebar Card */} +
+
+

+ Security Config +

+
+
+ Signing Secret + +
+
+
+ +
+

+ Subscriptions +

+
+ {webhook.events.map(ev => ( + + {ev.replace('.', ' ')} + + ))} +
+
+ + {webhook.permissions && webhook.permissions.length > 0 && ( +
+

+ Scopes +

+
+ {webhook.permissions.map(perm => ( + + {perm} + + ))} +
+
+ )} +
+ +
+
+ Created + {format(new Date(webhook.createdAt), "MMM d, yyyy")} +
+
+ Internal ID + {webhook.id} +
+
+ +
+
+
+
+ +
+ ); +} diff --git a/src/features/Webhooks/v1/pages/WebhookListPage.tsx b/src/features/Webhooks/v1/pages/WebhookListPage.tsx new file mode 100644 index 0000000..8bda0a5 --- /dev/null +++ b/src/features/Webhooks/v1/pages/WebhookListPage.tsx @@ -0,0 +1,273 @@ +import { useState } from "react"; +import { useWebhooks, useUpdateWebhook, useDeleteWebhook, useBulkWebhookAction } from "../hooks/useWebhooks"; +import { DEFAULT_WEBHOOK_FILTERS } from "../constants/webhook.constants"; +import type { Webhook, WebhookFilters } from "../Webhook.types"; +import WebhookHeader from "../components/layout/WebhookHeader"; +import WebhookFiltersBar from "../components/layout/WebhookFilters"; +import WebhookTable from "../components/table/WebhookTable"; +import WebhookCardList from "../components/table/WebhookCardList"; +import BulkActionBar from "../components/layout/BulkActionBar"; +import ConfirmModal from "@/features/Tasks/v1/components/common/ConfirmModal"; +import EmptyState from "@/features/Tasks/v1/components/common/EmptyState"; +import { ToastContainer, useToast } from "@/features/Tasks/v1/components/common/ToastNotification"; +import { useNavigate } from "react-router-dom"; +import { Plus } from "lucide-react"; + +export default function WebhookListPage() { + const navigate = useNavigate(); + const { toasts, addToast, dismiss } = useToast(); + + const [filters, setFilters] = useState(DEFAULT_WEBHOOK_FILTERS); + const [selectedIds, setSelectedIds] = useState([]); + + // Total count for header (no filters applied) + const { data: allPaginated = { data: [], total: 0, totalPages: 0 } } = useWebhooks(DEFAULT_WEBHOOK_FILTERS); + const allWebhooks = allPaginated.data; + const totalCount = allPaginated.total; + + // Filtered data + const { data: paginatedData, isLoading, isError, refetch } = useWebhooks(filters); + const webhooks = paginatedData?.data || []; + const totalPages = paginatedData?.totalPages || 0; + + const updateWebhook = useUpdateWebhook(); + const deleteWebhook = useDeleteWebhook(); + const bulkAction = useBulkWebhookAction(); + + const [webhookToDelete, setWebhookToDelete] = useState(null); + const [bulkActionToConfirm, setBulkActionToConfirm] = useState<"delete" | "enable" | "disable" | null>(null); + + const activeCount = allWebhooks.filter(w => w.status === "active").length; + + const handleToggleStatus = async (webhook: Webhook) => { + const newStatus = webhook.status === "active" ? "inactive" : "active"; + try { + await updateWebhook.mutateAsync({ + id: webhook.id, + payload: { status: newStatus } + }); + addToast("success", "Status updated", `Webhook is now ${newStatus}`); + } catch { + addToast("error", "Update failed", "Could not update status"); + } + }; + + const handleToggleSelect = (id: string) => { + setSelectedIds(prev => + prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + ); + }; + + const handleSelectAll = (ids: string[]) => { + setSelectedIds(ids); + }; + + const handleBulkAction = async (actionOverride?: "enable" | "disable") => { + const action = actionOverride || bulkActionToConfirm; + if (!action) return; + try { + await bulkAction.mutateAsync({ + ids: selectedIds, + action + }); + addToast( + "success", + "Bulk Action Successful", + `Successfully ${action}d ${selectedIds.length} webhooks.` + ); + setSelectedIds([]); + } catch { + addToast("error", "Bulk Action Failed", "Something went wrong."); + } finally { + setBulkActionToConfirm(null); + } + }; + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + setFilters(f => ({ ...f, page: newPage })); + } + }; + + const handleDelete = async () => { + if (!webhookToDelete) return; + try { + await deleteWebhook.mutateAsync(webhookToDelete.id); + addToast("success", "Webhook deleted", "The webhook has been permanently removed."); + } catch { + addToast("error", "Delete failed", "Something went wrong."); + } finally { + setWebhookToDelete(null); + } + }; + + return ( +
+ + +
+ {isError ? ( +
+ void refetch()} className="cd-btn cd-btn-secondary px-6 py-2.5 rounded-xl border"> + Retry + + } + /> +
+ ) : totalCount === 0 && !isLoading ? ( +
+ navigate("/org/dashboard/webhooks/create")} + className="cd-btn cd-btn-primary flex items-center gap-2 px-8 py-3 rounded-2xl shadow-xl shadow-[var(--cd-primary-subtle)] hover:scale-105 transition-all" + > + Create Webhook + + } + /> +
+ ) : ( +
+ setFilters({ ...newFilters, page: 1 })} + totalCount={totalCount} + filteredCount={paginatedData?.total || 0} + /> + +
+ {/* Desktop View */} +
+
+ + {webhooks.length === 0 && !isLoading && ( +
+ setFilters(DEFAULT_WEBHOOK_FILTERS)} + className="cd-btn cd-btn-secondary px-6 py-2 rounded-xl border text-sm font-medium" + > + Clear All Filters + + } + /> +
+ )} +
+
+ + {/* Mobile View */} +
+ +
+ + {/* Pagination UI */} + {totalPages > 1 && ( +
+
+ Showing page {filters.page} of {totalPages} +
+
+ + +
+
+ )} +
+
+ )} +
+ + void handleDelete()} + onCancel={() => setWebhookToDelete(null)} + isLoading={deleteWebhook.isPending} + danger + /> + + void handleBulkAction()} + onCancel={() => setBulkActionToConfirm(null)} + isLoading={bulkAction.isPending} + danger + /> + + setSelectedIds([])} + onAction={(action) => { + if (action === "delete") { + setBulkActionToConfirm("delete"); + } else { + handleBulkAction(action); + } + }} + /> + + +
+ ); +} diff --git a/src/features/Webhooks/v1/pages/WebhookLogsPage.tsx b/src/features/Webhooks/v1/pages/WebhookLogsPage.tsx new file mode 100644 index 0000000..afeb2d9 --- /dev/null +++ b/src/features/Webhooks/v1/pages/WebhookLogsPage.tsx @@ -0,0 +1,155 @@ +import { useState } from "react"; +import { useNavigate, useParams, Link } from "react-router-dom"; +import { ArrowLeft, Activity } from "lucide-react"; +import { useWebhook } from "../hooks/useWebhooks"; +import { useWebhookLogs, useRetryWebhookDelivery } from "../hooks/useWebhookLogs"; +import { DEFAULT_LOG_FILTERS } from "../constants/webhook.constants"; +import type { WebhookLogFilters } from "../Webhook.types"; +import LogsTable from "../components/table/LogsTable"; +import LogsCardList from "../components/table/LogsCardList"; +import { ToastContainer, useToast } from "@/features/Tasks/v1/components/common/ToastNotification"; +import { PillDropdown } from "../components/layout/WebhookFilters"; + +const LOG_STATUS_DOTS: Record = { all: "bg-gray-400", success: "bg-emerald-500", failed: "bg-red-500" }; +const LOG_STATUS_STYLES: Record = { + success: { bg: "var(--cd-success-subtle)", color: "var(--cd-success)", border: "var(--cd-success-subtle)" }, + failed: { bg: "var(--cd-danger-subtle)", color: "var(--cd-danger)", border: "var(--cd-danger-subtle)" }, +}; + +export default function WebhookLogsPage() { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const { toasts, addToast, dismiss } = useToast(); + + const [filters, setFilters] = useState(DEFAULT_LOG_FILTERS); + + const { data: webhook } = useWebhook(id); + const { data: paginatedData, isLoading } = useWebhookLogs(id, filters); + const logs = paginatedData?.data || []; + const totalPages = paginatedData?.totalPages || 0; + const retryMutation = useRetryWebhookDelivery(); + + const handleRetry = async (logId: string) => { + if (!id) return; + try { + await retryMutation.mutateAsync({ webhookId: id, logId }); + addToast("success", "Retry triggered", "The delivery has been queued for retry."); + } catch { + addToast("error", "Retry failed", "Could not trigger the retry mechanism."); + } + }; + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + setFilters(f => ({ ...f, page: newPage })); + } + }; + + return ( +
+ {/* Header */} +
+
+ Webhooks + / + {webhook?.name || 'Loading...'} + / + Logs +
+
+ +
+

+ Delivery Logs +

+
+
+
+ + {/* Filter Bar */} +
+ + label="Status" + value={filters.status} + dotMap={LOG_STATUS_DOTS} + styleMap={LOG_STATUS_STYLES} + onChange={(v) => setFilters(f => ({ ...f, status: v, page: 1 }))} + options={[ + { value: "all", label: "All Statuses" }, + { value: "success", label: "Success" }, + { value: "failed", label: "Failed" }, + ]} + /> + + label="Event" + value={filters.event} + dotMap={{ all: "bg-gray-400" }} + styleMap={{}} + onChange={(v) => setFilters(f => ({ ...f, event: v, page: 1 }))} + options={[ + { value: "all", label: "All Events" }, + ...(webhook?.events.map(ev => ({ value: ev, label: ev })) || []), + ]} + /> +
+ +
+ {/* Desktop View */} +
+
+ +
+
+ + {/* Mobile View */} +
+ +
+ + {/* Pagination UI */} + {totalPages > 1 && ( +
+
+ Showing page {filters.page} of {totalPages} +
+
+ + +
+
+ )} +
+ + +
+ ); +} diff --git a/src/main.tsx b/src/main.tsx index 5fab8ea..eb90262 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,4 @@ -import React from "react"; + import App from "./App"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createRoot } from "react-dom/client"; diff --git a/src/routes/OrgRoute.tsx b/src/routes/OrgRoute.tsx index e235e19..e39049d 100644 --- a/src/routes/OrgRoute.tsx +++ b/src/routes/OrgRoute.tsx @@ -1,3 +1,4 @@ +import { Suspense, lazy } from "react"; import { Route, Routes } from "react-router"; import AddMemberPage from "@/features/AddMember/v1/Page/AddMemberPage"; import Contact from "@/features/Contact_And_Support/v1/Pages/Contact"; @@ -12,39 +13,66 @@ import EditTaskPage from "@/features/Tasks/v1/pages/EditTaskPage"; import TaskDetailPage from "@/features/Tasks/v1/pages/TaskDetailPage"; import TaskManagementPage from "@/features/Tasks/v1/pages/TaskManagementPage"; +import ProtectedRoute from "./ProtectedRoute"; +import { dashboardData } from "@/features/Member/v1/mock/dashboardData"; + +// Lazy-loaded Webhook pages +const WebhookListPage = lazy(() => import("@/features/Webhooks/v1/pages/WebhookListPage")); +const CreateWebhookPage = lazy(() => import("@/features/Webhooks/v1/pages/CreateWebhookPage")); +const EditWebhookPage = lazy(() => import("@/features/Webhooks/v1/pages/EditWebhookPage")); +const WebhookDetailsPage = lazy(() => import("@/features/Webhooks/v1/pages/WebhookDetailsPage")); +const WebhookLogsPage = lazy(() => import("@/features/Webhooks/v1/pages/WebhookLogsPage")); + const OrgRoute = () => { return ( - + Loading...
}> + + + }> + {/* Dashboard */} + } /> - }> - {/* Dashboard */} - } /> + } /> - } /> + } /> - } /> + {/* Events */} + } /> - {/* Events */} - } /> + } /> - } /> + } /> - } /> + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> + {/* Webhooks */} + + + } /> + } /> + } /> + } /> + } /> + + + } + /> - {/* Contact */} - } /> + {/* Contact */} + } /> - {/* Add Member */} - } /> - + {/* Add Member */} + } /> + - - + + ); }; diff --git a/src/utils/reminders.ts b/src/utils/reminders.ts index d085563..4745685 100644 --- a/src/utils/reminders.ts +++ b/src/utils/reminders.ts @@ -41,5 +41,5 @@ export const getSmartReminders = (tasks: Task[]): Reminder[] => { }); //sorting acc to priority - return reminders.sort((a, b) => (a.type === "urgent" ? -1 : 1)).slice(0, 5); + return reminders.sort((a) => (a.type === "urgent" ? -1 : 1)).slice(0, 5); }; diff --git a/src/utils/telemetry.ts b/src/utils/telemetry.ts new file mode 100644 index 0000000..f95bdd5 --- /dev/null +++ b/src/utils/telemetry.ts @@ -0,0 +1,20 @@ +/** + * Simple Telemetry/Observability utility for frontend tracking. + * In a real-world scenario, this would wrap PostHog, Sentry, Datadog, etc. + */ + +export const Telemetry = { + trackAction: (actionName: string, metadata?: Record) => { + // e.g. posthog.capture(actionName, metadata) + console.info(`[Telemetry Action]: ${actionName}`, metadata || {}); + }, + + trackError: (errorName: string, error: any, metadata?: Record) => { + // e.g. Sentry.captureException(error) + console.error(`[Telemetry Error]: ${errorName}`, error, metadata || {}); + }, + + trackFormError: (formName: string, errors: Record) => { + console.warn(`[Telemetry Form Error]: Validation failed in ${formName}`, errors); + } +}; diff --git a/vite.config.ts b/vite.config.ts index ac8887a..7173232 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,6 +16,12 @@ export default defineConfig(async () => ({ }, }, + test: { + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"], + globals: true, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom';