diff --git a/SECURITY_VULNERABILITIES_ANALYSIS.md b/SECURITY_VULNERABILITIES_ANALYSIS.md new file mode 100644 index 00000000..50e8546f --- /dev/null +++ b/SECURITY_VULNERABILITIES_ANALYSIS.md @@ -0,0 +1,205 @@ +# Security Vulnerabilities Analysis + +## Overview +This document traces the origin of all security vulnerabilities identified in the project's dependency tree. + +## Vulnerability Summary +- **Total Vulnerabilities**: 14 (1 low, 13 high) +- **Fixed**: 9 vulnerabilities (reduced from 23) +- **Remaining**: 14 vulnerabilities + +--- + +## 1. `ip` Package Vulnerability (High Severity) + +### Vulnerability Details +- **CVE**: SSRF improper categorization in `isPublic` function +- **Affected Version**: `ip@2.0.1` +- **Risk**: Server-Side Request Forgery (SSRF) attacks possible + +### Dependency Chain +``` +Root Project +└── @meshsdk/react@1.9.0-beta.87 + └── @fabianbormann/cardano-peer-connect@1.2.18 + └── @fabianbormann/meerkat@1.0.18 + └── webtorrent@2.8.5 + ├── bittorrent-tracker@11.2.2 + │ └── ip@2.0.1 ⚠️ VULNERABLE + └── load-ip-set@3.0.1 + └── ip-set@2.2.0 + └── ip@2.0.1 ⚠️ VULNERABLE +``` + +### Root Cause +The `ip` package is pulled in by **WebTorrent**, which is used for peer-to-peer (P2P) connectivity in Cardano wallet connections. This is a transitive dependency from: +- `@fabianbormann/cardano-peer-connect` → Used by MeshSDK for P2P wallet connections +- `webtorrent` → BitTorrent protocol implementation for P2P networking +- `bittorrent-tracker` → Tracker client that uses `ip` for IP address validation + +### Impact Assessment +- **Usage**: Only used when P2P wallet connection features are active +- **Risk Level**: Medium-Low (only affects P2P connectivity features) +- **Attack Vector**: Requires attacker to control IP addresses in P2P network context + +### Mitigation Options +1. **Wait for upstream fix**: Monitor `@fabianbormann/cardano-peer-connect` for updates +2. **Use npm overrides**: Force a patched version of `ip` (risky, may break functionality) +3. **Disable P2P features**: If not needed, consider removing MeshSDK P2P functionality +4. **Contact maintainers**: Report to MeshSDK team about dependency updates + +--- + +## 2. `brace-expansion` Vulnerability (ReDoS) + +### Vulnerability Details +- **CVE**: Regular Expression Denial of Service (ReDoS) +- **Affected Versions**: `1.0.0 - 1.1.11 || 2.0.0 - 2.0.1` +- **Risk**: CPU exhaustion through malicious regex patterns + +### Dependency Chain +``` +Root Project +└── @meshsdk/core-cst@1.9.0-beta.87 + └── @cardano-sdk/crypto@0.2.3 + └── npm@9.9.4 ⚠️ BUNDLED DEPENDENCY + ├── minimatch@9.0.3 + │ └── brace-expansion@2.0.1 ⚠️ VULNERABLE + └── node-gyp@9.4.1 + ├── glob@7.2.3 + │ └── minimatch@3.1.2 + │ └── brace-expansion@1.1.11 ⚠️ VULNERABLE + └── cacache@16.1.3 + └── glob@8.1.0 + └── minimatch@5.1.6 + └── brace-expansion@2.0.1 ⚠️ VULNERABLE +``` + +### Root Cause +**Critical Finding**: `@cardano-sdk/crypto@0.2.3` includes `npm@^9.3.0` as a **production dependency**. This is highly unusual and problematic because: + +1. **npm should not be a dependency**: npm is a package manager, not a library +2. **Bundled vulnerabilities**: npm@9.9.4 bundles vulnerable versions of `brace-expansion` and `glob` +3. **Cannot be fixed via project dependencies**: These are bundled inside npm itself + +### Why npm is a dependency +The `@cardano-sdk/crypto` package likely uses npm for: +- Build tooling or scripts +- Package management utilities +- Development tooling (incorrectly marked as production dependency) + +**This is a bug/misconfiguration in the Cardano SDK package.** + +### Impact Assessment +- **Usage**: Likely only used during build/development, not runtime +- **Risk Level**: Low-Medium (ReDoS requires specific attack patterns) +- **Attack Vector**: Requires attacker to provide malicious input to brace expansion functions + +### Mitigation Options +1. **Update npm globally**: `npm install -g npm@latest` (fixes bundled dependencies) +2. **Report to Cardano SDK**: This is a packaging issue that should be fixed upstream +3. **Use npm overrides**: Force newer versions (may break npm functionality) +4. **Consider alternative**: Evaluate if `@cardano-sdk/crypto` is necessary or if there's an alternative + +--- + +## 3. `glob` Vulnerability (High Severity) + +### Vulnerability Details +- **CVE**: Command injection via `-c/--cmd` executes matches with `shell:true` +- **Affected Versions**: `glob@10.2.0 - 10.4.5` +- **Risk**: Command injection attacks + +### Dependency Chain +``` +Root Project +└── @meshsdk/core-cst@1.9.0-beta.87 + └── @cardano-sdk/crypto@0.2.3 + └── npm@9.9.4 ⚠️ BUNDLED DEPENDENCY + ├── glob@10.3.10 ⚠️ VULNERABLE + └── node-gyp@9.4.1 + └── (uses older glob versions) +``` + +### Root Cause +Same as `brace-expansion` - bundled in npm@9.9.4 which is incorrectly included as a dependency of `@cardano-sdk/crypto`. + +### Impact Assessment +- **Usage**: Only if npm CLI features are used at runtime (unlikely) +- **Risk Level**: Low (requires CLI usage with malicious input) +- **Attack Vector**: Command injection through glob CLI usage + +### Mitigation Options +Same as `brace-expansion` - update npm globally or report to Cardano SDK maintainers. + +--- + +## 4. Previously Fixed Vulnerabilities + +### ✅ `axios` (Fixed) +- **Was**: Vulnerable versions in `@cardano-sdk/util-dev` +- **Fixed**: Updated MeshSDK packages to `1.9.0-beta.87` +- **Status**: Resolved + +### ✅ `tar-fs` (Fixed) +- **Was**: Vulnerable versions in `dockerode` +- **Fixed**: Updated MeshSDK packages +- **Status**: Resolved + +--- + +## Recommendations + +### Immediate Actions +1. ✅ **Update MeshSDK packages** - Already completed (all at `1.9.0-beta.87`) +2. ⚠️ **Update npm globally**: `npm install -g npm@latest` +3. 📝 **Report to Cardano SDK**: File issue about npm being a production dependency + +### Long-term Actions +1. **Monitor dependencies**: Set up automated dependency scanning +2. **Evaluate alternatives**: Consider if Cardano SDK crypto package is necessary +3. **Review P2P features**: Assess if `cardano-peer-connect` is required for your use case +4. **Use npm overrides**: If needed, add overrides for critical vulnerabilities (with caution) + +### npm Overrides Example (Use with Caution) +```json +{ + "overrides": { + "ip": "^2.0.2", + "brace-expansion": "^2.0.2", + "glob": "^10.4.6" + } +} +``` + +--- + +## Dependency Tree Visualization + +### Critical Paths +``` +Root → @meshsdk/react → @fabianbormann/cardano-peer-connect → webtorrent → ip ⚠️ +Root → @meshsdk/core-cst → @cardano-sdk/crypto → npm → brace-expansion/glob ⚠️ +``` + +### Key Packages +- **@meshsdk/react**: Main MeshSDK React integration +- **@fabianbormann/cardano-peer-connect**: P2P wallet connectivity +- **@cardano-sdk/crypto**: Cryptographic utilities (incorrectly includes npm) +- **webtorrent**: BitTorrent protocol for P2P +- **npm**: Package manager (should not be a dependency!) + +--- + +## Conclusion + +The security vulnerabilities stem from: +1. **Transitive dependencies** in MeshSDK's P2P connectivity features (`ip` vulnerability) +2. **Packaging error** in Cardano SDK (`npm` as production dependency causing `brace-expansion`/`glob` issues) + +Most vulnerabilities are low-risk for production use, but should be addressed through: +- Updating npm globally +- Reporting issues to upstream maintainers +- Monitoring for package updates + + diff --git a/next.config.js b/next.config.js index 33101631..9ddc371b 100644 --- a/next.config.js +++ b/next.config.js @@ -12,11 +12,6 @@ const config = { defaultLocale: "en", }, transpilePackages: ["geist"], - eslint: { - // Warning: This allows production builds to successfully complete even if - // your project has ESLint errors. - ignoreDuringBuilds: true, - }, typescript: { // Warning: This allows production builds to successfully complete even if // your project has type errors. @@ -36,8 +31,18 @@ const config = { protocol: "https", hostname: "ipfs.io", }, + { + protocol: "https", + hostname: "gateway.pinata.cloud", + }, ], }, + // Turbopack configuration (Next.js 16+) + // Empty config silences the warning about webpack/turbopack conflict + // WebAssembly support is enabled by default in Turbopack + turbopack: {}, + + // Webpack config for builds that explicitly use webpack (e.g., with --webpack flag) webpack: function (config, options) { config.experiments = { asyncWebAssembly: true, diff --git a/package-lock.json b/package-lock.json index b2d518d5..8032921e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,6 @@ "@trpc/next": "^11.0.0-rc.446", "@trpc/react-query": "^11.0.0-rc.446", "@trpc/server": "^11.0.0-rc.446", - "@vercel/blob": "^0.23.4", "busboy": "^1.6.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -55,7 +54,7 @@ "jsonld": "^8.3.3", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.439.0", - "next": "^15.2.4", + "next": "^16.0.8", "next-auth": "^4.24.7", "papaparse": "^5.5.3", "react": "^18.3.1", @@ -96,7 +95,7 @@ "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", "eslint": "^9.39.1", - "eslint-config-next": "^15.5.7", + "eslint-config-next": "^16.0.8", "jest": "^30.1.3", "postcss": "^8.4.39", "prettier": "^3.3.2", @@ -3312,15 +3311,15 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.8.tgz", + "integrity": "sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz", - "integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.8.tgz", + "integrity": "sha512-1miV0qXDcLUaOdHridVPCh4i39ElRIAraseVIbb3BEqyZ5ol9sPyjTP/GNTPV5rBxqxjF6/vv5zQTVbhiNaLqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3328,9 +3327,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", - "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.8.tgz", + "integrity": "sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA==", "cpu": [ "arm64" ], @@ -3344,9 +3343,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", - "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.8.tgz", + "integrity": "sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA==", "cpu": [ "x64" ], @@ -3360,9 +3359,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", - "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.8.tgz", + "integrity": "sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ==", "cpu": [ "arm64" ], @@ -3376,9 +3375,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", - "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.8.tgz", + "integrity": "sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ==", "cpu": [ "arm64" ], @@ -3392,9 +3391,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", - "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.8.tgz", + "integrity": "sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng==", "cpu": [ "x64" ], @@ -3408,9 +3407,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", - "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.8.tgz", + "integrity": "sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA==", "cpu": [ "x64" ], @@ -3424,9 +3423,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", - "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.8.tgz", + "integrity": "sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A==", "cpu": [ "arm64" ], @@ -3440,9 +3439,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", - "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.8.tgz", + "integrity": "sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA==", "cpu": [ "x64" ], @@ -5152,13 +5151,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", - "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", - "dev": true, - "license": "MIT" - }, "node_modules/@rvagg/ripemd160": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@rvagg/ripemd160/-/ripemd160-2.2.4.tgz", @@ -6670,18 +6662,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", + "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/type-utils": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -6694,22 +6685,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.1", + "@typescript-eslint/parser": "^8.49.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", + "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4" }, "engines": { @@ -6725,14 +6716,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", + "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/tsconfig-utils": "^8.49.0", + "@typescript-eslint/types": "^8.49.0", "debug": "^4.3.4" }, "engines": { @@ -6747,14 +6738,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", + "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6765,9 +6756,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", + "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", "dev": true, "license": "MIT", "engines": { @@ -6782,15 +6773,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", + "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -6807,9 +6798,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", "dev": true, "license": "MIT", "engines": { @@ -6821,16 +6812,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", + "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/project-service": "8.49.0", + "@typescript-eslint/tsconfig-utils": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -6862,16 +6853,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", + "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6886,13 +6877,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", + "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/types": "8.49.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -7234,22 +7225,6 @@ "node": ">=20.0.0" } }, - "node_modules/@vercel/blob": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-0.23.4.tgz", - "integrity": "sha512-cOU2e01RWZXFyc/OVRq+zZg38m34bcxpQk5insKp3Td9akNWThrXiF2URFHpRlm4fbaQ/l7pPSOB5nkLq+t6pw==", - "license": "Apache-2.0", - "dependencies": { - "async-retry": "^1.3.3", - "bytes": "^3.1.2", - "is-buffer": "^2.0.5", - "is-plain-object": "^5.0.0", - "undici": "^5.28.4" - }, - "engines": { - "node": ">=16.14" - } - }, "node_modules/@webgpu/types": { "version": "0.1.67", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.67.tgz", @@ -7670,15 +7645,6 @@ "node": ">= 0.4" } }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "license": "MIT", - "dependencies": { - "retry": "0.13.1" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -8704,15 +8670,6 @@ "node": ">=10.16.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -10705,25 +10662,24 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.7.tgz", - "integrity": "sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.8.tgz", + "integrity": "sha512-8J5cOAboXIV3f8OD6BOyj7Fik6n/as7J4MboiUSExWruf/lCu1OPR3ZVSdnta6WhzebrmAATEmNSBZsLWA6kbg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.7", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@next/eslint-plugin-next": "16.0.8", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -10732,6 +10688,19 @@ } } }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -10973,13 +10942,20 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" @@ -12154,13 +12130,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/h3-js": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.3.0.tgz", @@ -12417,6 +12386,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -12827,29 +12813,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/is-bun-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", @@ -13100,15 +13063,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -15993,12 +15947,12 @@ } }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.8.tgz", + "integrity": "sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "16.0.8", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -16008,18 +15962,18 @@ "next": "dist/bin/next" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.7", - "@next/swc-darwin-x64": "15.5.7", - "@next/swc-linux-arm64-gnu": "15.5.7", - "@next/swc-linux-arm64-musl": "15.5.7", - "@next/swc-linux-x64-gnu": "15.5.7", - "@next/swc-linux-x64-musl": "15.5.7", - "@next/swc-win32-arm64-msvc": "15.5.7", - "@next/swc-win32-x64-msvc": "15.5.7", - "sharp": "^0.34.3" + "@next/swc-darwin-arm64": "16.0.8", + "@next/swc-darwin-x64": "16.0.8", + "@next/swc-linux-arm64-gnu": "16.0.8", + "@next/swc-linux-arm64-musl": "16.0.8", + "@next/swc-linux-x64-gnu": "16.0.8", + "@next/swc-linux-x64-musl": "16.0.8", + "@next/swc-win32-arm64-msvc": "16.0.8", + "@next/swc-win32-x64-msvc": "16.0.8", + "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -21447,15 +21401,6 @@ "node": ">=4" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -22963,6 +22908,22 @@ "node": ">=8.10.0" } }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -23836,6 +23797,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", + "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.49.0", + "@typescript-eslint/parser": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -25092,6 +25077,19 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/package.json b/package.json index d202e63a..b87a4c10 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "@trpc/next": "^11.0.0-rc.446", "@trpc/react-query": "^11.0.0-rc.446", "@trpc/server": "^11.0.0-rc.446", - "@vercel/blob": "^0.23.4", "busboy": "^1.6.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -67,7 +66,7 @@ "jsonld": "^8.3.3", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.439.0", - "next": "^15.2.4", + "next": "^16.0.8", "next-auth": "^4.24.7", "papaparse": "^5.5.3", "react": "^18.3.1", @@ -108,7 +107,7 @@ "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", "eslint": "^9.39.1", - "eslint-config-next": "^15.5.7", + "eslint-config-next": "^16.0.8", "jest": "^30.1.3", "postcss": "^8.4.39", "prettier": "^3.3.2", diff --git a/prisma/migrations/20250101000000_add_crowdfund_gov_extension_table/migration.sql b/prisma/migrations/20250101000000_add_crowdfund_gov_extension_table/migration.sql new file mode 100644 index 00000000..3dac921b --- /dev/null +++ b/prisma/migrations/20250101000000_add_crowdfund_gov_extension_table/migration.sql @@ -0,0 +1,7 @@ +-- This migration was applied to the database but the migration file was not found locally. +-- The migration failed and has been marked as rolled back. +-- If you need to recreate this migration, please check the database schema or git history. + +-- No-op migration (failed and rolled back) +SELECT 1; + diff --git a/prisma/migrations/20250806050340_add_crowdfund/migration.sql b/prisma/migrations/20250806050340_add_crowdfund/migration.sql new file mode 100644 index 00000000..5da9a2c3 --- /dev/null +++ b/prisma/migrations/20250806050340_add_crowdfund/migration.sql @@ -0,0 +1,7 @@ +-- This migration was applied to the database but the migration file was not found locally. +-- The migration has been marked as applied in the database. +-- If you need to recreate this migration, please check the database schema or git history. + +-- No-op migration (already applied) +SELECT 1; + diff --git a/prisma/migrations/20250825090537_add_param_utxo/migration.sql b/prisma/migrations/20250825090537_add_param_utxo/migration.sql new file mode 100644 index 00000000..5da9a2c3 --- /dev/null +++ b/prisma/migrations/20250825090537_add_param_utxo/migration.sql @@ -0,0 +1,7 @@ +-- This migration was applied to the database but the migration file was not found locally. +-- The migration has been marked as applied in the database. +-- If you need to recreate this migration, please check the database schema or git history. + +-- No-op migration (already applied) +SELECT 1; + diff --git a/prisma/migrations/20250828113754_add_date_crowdfund/migration.sql b/prisma/migrations/20250828113754_add_date_crowdfund/migration.sql new file mode 100644 index 00000000..5da9a2c3 --- /dev/null +++ b/prisma/migrations/20250828113754_add_date_crowdfund/migration.sql @@ -0,0 +1,7 @@ +-- This migration was applied to the database but the migration file was not found locally. +-- The migration has been marked as applied in the database. +-- If you need to recreate this migration, please check the database schema or git history. + +-- No-op migration (already applied) +SELECT 1; + diff --git a/prisma/migrations/20250909122657_add_gov_crowdfund/migration.sql b/prisma/migrations/20250909122657_add_gov_crowdfund/migration.sql new file mode 100644 index 00000000..5da9a2c3 --- /dev/null +++ b/prisma/migrations/20250909122657_add_gov_crowdfund/migration.sql @@ -0,0 +1,7 @@ +-- This migration was applied to the database but the migration file was not found locally. +-- The migration has been marked as applied in the database. +-- If you need to recreate this migration, please check the database schema or git history. + +-- No-op migration (already applied) +SELECT 1; + diff --git a/prisma/migrations/20251113150348_add_ipfs_hash_table/migration.sql b/prisma/migrations/20251113150348_add_ipfs_hash_table/migration.sql new file mode 100644 index 00000000..5da9a2c3 --- /dev/null +++ b/prisma/migrations/20251113150348_add_ipfs_hash_table/migration.sql @@ -0,0 +1,7 @@ +-- This migration was applied to the database but the migration file was not found locally. +-- The migration has been marked as applied in the database. +-- If you need to recreate this migration, please check the database schema or git history. + +-- No-op migration (already applied) +SELECT 1; + diff --git a/prisma/migrations/20251128091925_add_gov_action_anchor_field/migration.sql b/prisma/migrations/20251128091925_add_gov_action_anchor_field/migration.sql new file mode 100644 index 00000000..91761288 --- /dev/null +++ b/prisma/migrations/20251128091925_add_gov_action_anchor_field/migration.sql @@ -0,0 +1,17 @@ +-- AlterTable +-- Add anchorUrls column if it doesn't exist +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='Ballot' AND column_name='anchorUrls') THEN + ALTER TABLE "Ballot" ADD COLUMN "anchorUrls" TEXT[]; + END IF; +END $$; + +-- Add anchorHashes column if it doesn't exist +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='Ballot' AND column_name='anchorHashes') THEN + ALTER TABLE "Ballot" ADD COLUMN "anchorHashes" TEXT[]; + END IF; +END $$; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 47e88233..268ef641 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -97,6 +97,8 @@ model Ballot { items String[] itemDescriptions String[] choices String[] + anchorUrls String[] @default([]) + anchorHashes String[] @default([]) type Int createdAt DateTime @default(now()) } diff --git a/src/components/common/ImgDragAndDrop.tsx b/src/components/common/ImgDragAndDrop.tsx index 1e5b82ff..e93e722d 100644 --- a/src/components/common/ImgDragAndDrop.tsx +++ b/src/components/common/ImgDragAndDrop.tsx @@ -39,7 +39,7 @@ export default function ImgDragAndDrop({ onImageUpload }: ImgDragAndDropProps) { async function checkImageExists(shortHash: string): Promise { try { - const response = await fetch(`/api/vercel-storage/image/exists?shortHash=${shortHash}`, { + const response = await fetch(`/api/pinata-storage/image/exists?shortHash=${shortHash}`, { method: "GET", }); if (response.ok) { @@ -81,7 +81,7 @@ export default function ImgDragAndDrop({ onImageUpload }: ImgDragAndDropProps) { formData.append("shortHash", shortHash); formData.append("filename", file.name); - const response = await fetch("/api/vercel-storage/image/put", { + const response = await fetch("/api/pinata-storage/image/put", { method: "POST", body: formData, }); diff --git a/src/components/common/overall-layout/layout.tsx b/src/components/common/overall-layout/layout.tsx index 1b88e853..b85b2fd6 100644 --- a/src/components/common/overall-layout/layout.tsx +++ b/src/components/common/overall-layout/layout.tsx @@ -10,6 +10,7 @@ import { useUserStore } from "@/lib/zustand/user"; import useAppWallet from "@/hooks/useAppWallet"; import { useWalletContext, WalletState } from "@/hooks/useWalletContext"; import useMultisigWallet from "@/hooks/useMultisigWallet"; +import { AlertCircle, RefreshCw } from "lucide-react"; import SessionProvider from "@/components/SessionProvider"; import { getServerSession } from "next-auth"; @@ -29,6 +30,8 @@ import dynamic from "next/dynamic"; import Loading from "@/components/common/overall-layout/loading"; import { MobileNavigation } from "@/components/ui/mobile-navigation"; import { MobileActionsMenu } from "@/components/ui/mobile-actions-menu"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; // Dynamically import ConnectWallet with SSR disabled to avoid production SSR issues // Using a version-based key ensures fresh mount on updates, preventing cache issues @@ -360,15 +363,35 @@ export default function RootLayout({
-

Something went wrong

-

Please try refreshing the page or reconnecting your wallet.

- +
+ + +
+
+
+ +
+
+ +
+

+ Something went wrong +

+

+ Please try refreshing the page or reconnecting your wallet. +

+
+ + + +
} > diff --git a/src/components/common/overall-layout/proxy-data-loader.tsx b/src/components/common/overall-layout/proxy-data-loader.tsx index dd81308b..962b32c5 100644 --- a/src/components/common/overall-layout/proxy-data-loader.tsx +++ b/src/components/common/overall-layout/proxy-data-loader.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import useAppWallet from "@/hooks/useAppWallet"; -import { useProxyData, useProxyActions } from "@/lib/zustand/proxy"; +import { useProxyData, useProxyActions, useProxyStore } from "@/lib/zustand/proxy"; import { useSiteStore } from "@/lib/zustand/site"; import { api } from "@/utils/api"; @@ -46,7 +46,8 @@ export default function ProxyDataLoader() { setProxies(appWallet.id, proxyData); } - }, [apiProxies, appWallet?.id, setProxies]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [apiProxies, appWallet?.id]); // setProxies is stable from Zustand, no need to include // Fetch additional data for each proxy useEffect(() => { @@ -68,16 +69,24 @@ export default function ProxyDataLoader() { proxy.paramUtxo, true, ); - await fetchProxyDelegatorsInfo( - appWallet.id, - proxy.id, - proxy.proxyAddress, - proxy.authTokenId, - appWallet.scriptCbor, - network.toString(), - proxy.paramUtxo, - true, - ); + // Only fetch delegators if DRep is registered + // Check if we have drepInfo and it's not null (meaning DRep is registered) + const currentProxies = useProxyStore.getState().proxies[appWallet.id] || []; + const currentProxy = currentProxies.find(p => p.id === proxy.id); + if (currentProxy?.drepInfo !== null && currentProxy?.drepInfo !== undefined) { + await fetchProxyDelegatorsInfo( + appWallet.id, + proxy.id, + proxy.proxyAddress, + proxy.authTokenId, + appWallet.scriptCbor, + network.toString(), + proxy.paramUtxo, + true, + ); + } else { + console.log(`Skipping delegators fetch for proxy ${proxy.id} - DRep not registered`); + } } catch (error) { console.error(`Error fetching data for proxy ${proxy.id}:`, error); } @@ -85,7 +94,8 @@ export default function ProxyDataLoader() { } })(); } - }, [proxies, appWallet?.id, appWallet?.scriptCbor, network, fetchProxyBalance, fetchProxyDrepInfo, fetchProxyDelegatorsInfo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [proxies, appWallet?.id, appWallet?.scriptCbor, network]); // Zustand actions are stable, no need to include them // Clear proxy data when wallet changes useEffect(() => { diff --git a/src/components/common/row-label-info.tsx b/src/components/common/row-label-info.tsx index c3bb73c2..2ffecbe3 100644 --- a/src/components/common/row-label-info.tsx +++ b/src/components/common/row-label-info.tsx @@ -18,11 +18,14 @@ export default function RowLabelInfo({ allowOverflow?: boolean; }) { const { toast } = useToast(); + // For mobile, stack vertically when allowOverflow is true (for long content like markdown) + const isStacked = allowOverflow; + return ( -
-
+
+
{label && ( -
+
{label}
)} @@ -37,7 +40,7 @@ export default function RowLabelInfo({ duration: 5000, }); }} - className="m-0 h-auto max-w-full justify-start truncate p-0" + className="m-0 h-auto max-w-full justify-start truncate p-0 text-xs sm:text-sm" > +
{value}
); diff --git a/src/components/multisig/proxy/offchain.ts b/src/components/multisig/proxy/offchain.ts index d0f5ebff..a936bd1d 100644 --- a/src/components/multisig/proxy/offchain.ts +++ b/src/components/multisig/proxy/offchain.ts @@ -711,6 +711,19 @@ export class MeshProxyContract extends MeshTxInitiator { getDrepDelegators = async (forceRefresh = false) => { const drepId = this.getDrepId(); + // First check if DRep is registered - don't fetch delegators if not registered + const drepStatus = await this.getDrepStatus(forceRefresh); + if (!drepStatus || drepStatus === null) { + // DRep is not registered, return empty result + console.log(`DRep ${drepId} is not registered, skipping delegators fetch`); + return { + delegators: [], + totalDelegation: "0", + totalDelegationADA: 0, + count: 0 + }; + } + // Check cache first const cacheKey = `${drepId}_delegators`; const cached = drepStatusCache.get(cacheKey); diff --git a/src/components/pages/homepage/wallets/SectionExplanation.tsx b/src/components/pages/homepage/wallets/SectionExplanation.tsx index d14f5627..4bb1c540 100644 --- a/src/components/pages/homepage/wallets/SectionExplanation.tsx +++ b/src/components/pages/homepage/wallets/SectionExplanation.tsx @@ -19,3 +19,4 @@ export default function SectionExplanation({ ); } + diff --git a/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx b/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx index ce5d487a..7259098f 100644 --- a/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx +++ b/src/components/pages/homepage/wallets/WalletBalanceSkeleton.tsx @@ -13,3 +13,4 @@ export default function WalletBalanceSkeleton() { ); } + diff --git a/src/components/pages/wallet/governance/ballot/BallotModal.tsx b/src/components/pages/wallet/governance/ballot/BallotModal.tsx new file mode 100644 index 00000000..0f4af635 --- /dev/null +++ b/src/components/pages/wallet/governance/ballot/BallotModal.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import BallotCard from "./ballot"; +import type { UTxO } from "@meshsdk/core"; +import { Vote as VoteIcon } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; + +interface BallotModalProps { + appWallet: any; + selectedBallotId?: string; + onSelectBallot: (id: string) => void; + utxos: UTxO[]; + open: boolean; + onOpenChange: (open: boolean) => void; + /** + * Optional current proposal context (used on the proposal page). + * When provided, the ballot card can show contextual UI like + * an "Add to ballot" button and highlighting. + */ + currentProposalId?: string; + currentProposalTitle?: string; + onBallotChanged?: () => void; +} + +export default function BallotModal({ + appWallet, + selectedBallotId, + onSelectBallot, + utxos, + open, + onOpenChange, + currentProposalId, + currentProposalTitle, + onBallotChanged, +}: BallotModalProps) { + return ( + + + +
+
+
+
+ +
+ + Manage Ballots + +
+ + Organize and vote on governance proposals + +
+
+
+
+
+ { + onBallotChanged?.(); + }} + /> +
+
+
+
+ ); +} diff --git a/src/components/pages/wallet/governance/ballot/FloatingBallotSidebar.tsx b/src/components/pages/wallet/governance/ballot/FloatingBallotSidebar.tsx index ded4ab24..66485be6 100644 --- a/src/components/pages/wallet/governance/ballot/FloatingBallotSidebar.tsx +++ b/src/components/pages/wallet/governance/ballot/FloatingBallotSidebar.tsx @@ -92,23 +92,23 @@ export default function FloatingBallotSidebar({ {open && ( -
-
- Your Ballots +
+
+ Your Ballots {proposalCount > 0 && ( {proposalCount} )}
-
+
void) | undefined, + currentProposalId: string | undefined, +) { + const handleBallotSelect = useCallback((ballotId: string) => { + if (onSelectBallot && ballotId !== selectedBallotId) { + onSelectBallot(ballotId); + } + }, [onSelectBallot, selectedBallotId]); + + const selectedBallot = useMemo(() => { + return selectedBallotId + ? ballots.find((b) => b.id === selectedBallotId) + : undefined; + }, [ballots, selectedBallotId]); + + return { + selectedBallot, + handleBallotSelect, + }; +} + +// Custom hook for proposal removal logic +function useProposalRemoval( + ballotId: string, + refetchBallots: () => Promise, + onBallotChanged?: () => void, +) { + const { toast } = useToast(); + const removeProposalMutation = api.ballot.removeProposalFromBallot.useMutation(); + const [removingIdx, setRemovingIdx] = React.useState(null); + const [deleteProposalIdx, setDeleteProposalIdx] = React.useState(null); + + const handleDelete = useCallback(async (idx: number) => { + if (removingIdx !== null) return; // Prevent multiple simultaneous deletions + + setRemovingIdx(idx); + setDeleteProposalIdx(null); // Close confirmation dialog + + try { + await removeProposalMutation.mutateAsync({ ballotId, index: idx }); + // Wait a bit for the mutation to complete before refetching + await new Promise(resolve => setTimeout(resolve, 100)); + await refetchBallots(); + onBallotChanged?.(); + + toast({ + title: "Proposal Removed", + description: "The proposal has been removed from the ballot.", + duration: 2000, + }); + } catch (error: unknown) { + toast({ + title: "Error", + description: "Failed to remove proposal from ballot.", + variant: "destructive", + }); + } finally { + setRemovingIdx(null); + } + }, [ballotId, removingIdx, removeProposalMutation, refetchBallots, onBallotChanged, toast]); + + const requestDelete = useCallback((idx: number) => { + setDeleteProposalIdx(idx); + }, []); + + const cancelDelete = useCallback(() => { + setDeleteProposalIdx(null); + }, []); + + return { + removingIdx, + deleteProposalIdx, + handleDelete, + requestDelete, + cancelDelete, + isRemoving: removingIdx !== null, + }; +} + export default function BallotCard({ appWallet, onSelectBallot, @@ -59,11 +149,6 @@ export default function BallotCard({ selectedBallotId?: string; onBallotChanged?: () => void; utxos: UTxO[]; - /** - * Optional current proposal context from the proposal page. - * When provided, the ballot card can render an "Add to ballot" - * button and highlight that proposal in the table. - */ currentProposalId?: string; currentProposalTitle?: string; }) { @@ -72,12 +157,11 @@ export default function BallotCard({ const [ballots, setBallots] = useState([]); const [creating, setCreating] = useState(false); - // State for adding the current proposal to a ballot (including move flow) - const [moveModal, setMoveModal] = useState<{ - targetBallotId: string; - conflictBallots: BallotType[]; + + const [deleteConfirmModal, setDeleteConfirmModal] = useState<{ + ballotId: string; + ballotDescription: string; } | null>(null); - const [moveLoading, setMoveLoading] = useState(false); const { toast } = useToast(); @@ -103,7 +187,7 @@ export default function BallotCard({ { enabled: !!(appWallet?.id || userAddress) } ); - // Check if we have valid proxy data (proxy enabled, selected, proxies exist, and selected proxy is found) + // Check if we have valid proxy data const hasValidProxy = !!(isProxyEnabled && selectedProxyId && proxies && proxies.length > 0 && proxies.find((p: any) => p.id === selectedProxyId)); // CreateBallot mutation @@ -121,8 +205,11 @@ export default function BallotCard({ const addProposalMutation = api.ballot.addProposalToBallot.useMutation(); const moveRemoveProposalMutation = api.ballot.removeProposalFromBallot.useMutation(); + const autoHandleRef = React.useRef(null); + const handledProposalRef = React.useRef>(new Set()); + const isProcessingRef = React.useRef(false); - // Refresh ballots after submit or on load and ensure a sensible default is selected + // Refresh ballots after submit or on load and auto-select first ballot if none selected React.useEffect(() => { if (!getBallots.data) return; @@ -133,40 +220,39 @@ export default function BallotCard({ ); setBallots(sorted); - // If nothing is selected yet (or the selection no longer exists), - // automatically select a sensible default: - // 1. Prefer the ballot that already contains the current proposal (when on a proposal page) - // 2. Otherwise, prefer the newest ballot that already has proposals - // 3. Fallback to the newest ballot + // Auto-select first ballot if none selected if (!onSelectBallot) return; const hasSelected = selectedBallotId && sorted.some((b) => b.id === selectedBallotId); - if (!hasSelected) { + if (!hasSelected && sorted.length > 0) { + // If we have a current proposal, try to find a ballot that contains it let ballotToSelect: BallotType | undefined; - + if (currentProposalId) { ballotToSelect = sorted.find( (b) => Array.isArray(b.items) && b.items.includes(currentProposalId), ); } - - if (!ballotToSelect) { - ballotToSelect = - sorted.find( - (b) => Array.isArray(b.items) && b.items.length > 0, - ) ?? sorted[0]; + + // Otherwise, select the first (newest) ballot + if (sorted.length > 0) { + onSelectBallot(ballotToSelect?.id || sorted[0]?.id || ""); } - - if (!ballotToSelect) return; - - onSelectBallot(ballotToSelect.id); } }, [getBallots.data, onSelectBallot, selectedBallotId, currentProposalId]); + // Use ballot switching hook + const { selectedBallot, handleBallotSelect } = useBallotSwitching( + ballots, + selectedBallotId, + onSelectBallot, + currentProposalId, + ); + - // Proxy ballot vote submission logic + // Proxy vote submission logic async function handleSubmitProxyVote() { if (!selectedBallot || !Array.isArray(selectedBallot.items) || selectedBallot.items.length === 0) { toast({ @@ -184,21 +270,22 @@ export default function BallotCard({ }); return; } - if (!hasValidProxy) { - // Fall back to standard vote if no valid proxy - return handleSubmitVote(); + if (!hasValidProxy || !selectedProxyId || !proxies) { + toast({ + title: "Proxy not configured", + description: "Please select a proxy to continue.", + duration: 2000, + variant: "destructive", + }); + return; } setLoading(true); try { - // Get the selected proxy - const proxy = proxies?.find((p: any) => p.id === selectedProxyId); - if (!proxy) { - // Fall back to standard vote if proxy not found - return handleSubmitVote(); - } + const proxy = proxies.find((p: any) => p.id === selectedProxyId); + if (!proxy) throw new Error("Proxy not found"); - // Create proxy contract instance + if (!multisigWallet) throw new Error("Multisig Wallet could not be built."); const meshTxBuilder = getTxBuilder(network); const proxyContract = new MeshProxyContract( { @@ -214,10 +301,17 @@ export default function BallotCard({ proxyContract.proxyAddress = proxy.proxyAddress; // Prepare votes array - const votes = selectedBallot.items.map((proposalId: string, index: number) => ({ - proposalId, - voteKind: (selectedBallot.choices?.[index] ?? "Abstain") as "Yes" | "No" | "Abstain", - })); + const votes = selectedBallot.items.map((proposalId: string, index: number) => { + const anchorUrl = selectedBallot.anchorUrls?.[index]; + const anchorHash = selectedBallot.anchorHashes?.[index]; + return { + proposalId, + voteKind: (selectedBallot.choices?.[index] ?? "Abstain") as "Yes" | "No" | "Abstain", + ...(anchorUrl && anchorHash + ? { anchor: { anchorUrl, anchorDataHash: anchorHash } } + : {}), + }; + }); // Vote using proxy const txBuilder = await proxyContract.voteProxyDrep(votes, utxos, multisigWallet?.getScript().address); @@ -361,6 +455,8 @@ export default function BallotCard({ // Skip invalid proposalId continue; } + const anchorUrl = selectedBallot.anchorUrls?.[i]; + const anchorHash = selectedBallot.anchorHashes?.[i]; txBuilder.vote( { type: "DRep", @@ -372,6 +468,9 @@ export default function BallotCard({ }, { voteKind: voteKind, + ...(anchorUrl && anchorHash + ? { anchor: { anchorUrl, anchorDataHash: anchorHash } } + : {}), }, ); } @@ -383,7 +482,6 @@ export default function BallotCard({ await newTransaction({ txBuilder, description: `Ballot Vote: ${selectedBallot.description || ""}`, - // Optionally add metadata (not implemented here) }); toast({ @@ -393,7 +491,6 @@ export default function BallotCard({ }); setAlert("Ballot vote transaction successfully created!"); - // Optionally refresh ballots await getBallots.refetch(); onBallotChanged?.(); } catch (error: unknown) { @@ -456,199 +553,241 @@ export default function BallotCard({ await getBallots.refetch(); onBallotChanged?.(); } catch (error: unknown) { - // TODO: handle error + toast({ + title: "Error", + description: "Failed to create ballot. Please try again.", + variant: "destructive", + }); } setSubmitting(false); } - // Find the selected ballot if selectedBallotId is set - const selectedBallot: BallotType | undefined = selectedBallotId - ? ballots.find((b) => b.id === selectedBallotId) - : undefined; - - const currentProposalAlreadyInSelected = - !!currentProposalId && - !!selectedBallot && - Array.isArray(selectedBallot.items) && - selectedBallot.items.includes(currentProposalId); - - async function performAddCurrentProposal(targetBallotId: string) { - if (!currentProposalId) return; - await addProposalMutation.mutateAsync({ - ballotId: targetBallotId, - itemDescription: - currentProposalTitle ?? - (selectedBallot?.description ?? "Untitled proposal"), - item: currentProposalId, - // Default to Abstain; user can fine-tune per-proposal choice in the table. - choice: "Abstain", - }); - } - - async function handleAddCurrentProposalToSelectedBallot() { + // Auto-add current proposal to selected ballot when modal opens + React.useEffect(() => { if (!currentProposalId || !selectedBallotId || !getBallots.data) return; + if (isProcessingRef.current) return; // Prevent concurrent processing + if (handledProposalRef.current.has(currentProposalId)) return; + const autoKey = `${selectedBallotId}-${currentProposalId}`; + if (autoHandleRef.current === autoKey) return; - // Ensure a proposal can only exist on a single ballot at a time. - const ballotsWithProposal = - getBallots.data.filter( - (b) => - b.id !== selectedBallotId && - Array.isArray(b.items) && - b.items.includes(currentProposalId), - ) ?? []; + const selectedBallot = getBallots.data.find((b) => b.id === selectedBallotId); + if (!selectedBallot) return; - if (ballotsWithProposal.length > 0) { - setMoveModal({ - targetBallotId: selectedBallotId, - conflictBallots: ballotsWithProposal, - }); + // Check if proposal is already in this ballot + const alreadyInBallot = Array.isArray(selectedBallot.items) && + selectedBallot.items.includes(currentProposalId); + + if (alreadyInBallot) { + autoHandleRef.current = autoKey; + handledProposalRef.current.add(currentProposalId); return; } - await performAddCurrentProposal(selectedBallotId); - toast({ - title: "Added to Ballot", - description: "Proposal successfully added to the ballot.", - duration: 800, - }); - await getBallots.refetch(); - onBallotChanged?.(); - } - - async function confirmMoveCurrentProposal() { - if (!moveModal || !currentProposalId) return; + // Set processing flag + isProcessingRef.current = true; + autoHandleRef.current = autoKey; - try { - setMoveLoading(true); + // Check if proposal is in another ballot + const ballotsWithProposal = getBallots.data.filter( + (b) => + b.id !== selectedBallotId && + Array.isArray(b.items) && + b.items.includes(currentProposalId), + ); - // Remove proposal from all other ballots before adding to the selected one - for (const b of moveModal.conflictBallots) { - const index = b.items.findIndex( - (item: string) => item === currentProposalId, - ); - if (index >= 0) { - await moveRemoveProposalMutation.mutateAsync({ - ballotId: b.id, - index, - }); - } - } + // If in another ballot, automatically move it + if (ballotsWithProposal.length > 0) { + Promise.all( + ballotsWithProposal.map(async (b) => { + const index = b.items.findIndex((item: string) => item === currentProposalId); + if (index >= 0) { + await moveRemoveProposalMutation.mutateAsync({ + ballotId: b.id, + index, + }); + } + }) + ).then(() => { + addProposalMutation.mutate({ + ballotId: selectedBallotId, + itemDescription: currentProposalTitle || "Untitled proposal", + item: currentProposalId, + choice: "Abstain", + }, { + onSuccess: () => { + handledProposalRef.current.add(currentProposalId); + isProcessingRef.current = false; + toast({ + title: "Moved to Ballot", + description: "Proposal moved to the selected ballot.", + duration: 2000, + }); + getBallots.refetch(); + onBallotChanged?.(); + }, + onError: () => { + isProcessingRef.current = false; + autoHandleRef.current = null; // Reset on error + }, + }); + }).catch(() => { + isProcessingRef.current = false; + autoHandleRef.current = null; + }); + return; + } - await performAddCurrentProposal(moveModal.targetBallotId); + // Auto-add to selected ballot + addProposalMutation.mutate({ + ballotId: selectedBallotId, + itemDescription: currentProposalTitle || "Untitled proposal", + item: currentProposalId, + choice: "Abstain", + }, { + onSuccess: () => { + handledProposalRef.current.add(currentProposalId); + isProcessingRef.current = false; + toast({ + title: "Added to Ballot", + description: "Proposal added to the selected ballot.", + duration: 2000, + }); + getBallots.refetch(); + onBallotChanged?.(); + }, + onError: (error: any) => { + isProcessingRef.current = false; + autoHandleRef.current = null; // Reset on error + toast({ + title: "Error", + description: error?.message || "Failed to add proposal to ballot.", + variant: "destructive", + }); + }, + }); + }, [currentProposalId, selectedBallotId, getBallots.data, currentProposalTitle]); - toast({ - title: "Proposal moved", - description: - "Proposal was moved from the other ballot to the selected ballot.", - duration: 1500, - }); - await getBallots.refetch(); - onBallotChanged?.(); - } finally { - setMoveLoading(false); - setMoveModal(null); + // Calculate vote summary + const voteSummary = useMemo(() => { + if (!selectedBallot || !Array.isArray(selectedBallot.items)) { + return { total: 0, yes: 0, no: 0, abstain: 0 }; } - } + const choices = selectedBallot.choices || []; + return { + total: selectedBallot.items.length, + yes: choices.filter((c: string) => c === "Yes").length, + no: choices.filter((c: string) => c === "No").length, + abstain: choices.filter((c: string) => c === "Abstain").length, + }; + }, [selectedBallot]); + return ( - -
-
+
+ {/* Ballot Tabs */} +
+
{ballots.map((b) => ( - ))} + ))} +
+
+ + {/* Create Ballot Input */} + {creating && ( +
+ setDescription(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && description.trim() && !submitting) { + handleSubmit(e); + } + if (e.key === "Escape") { setCreating(false); setDescription(""); - } else { - // Show the create-input row - setCreating(true); } }} + disabled={submitting} + autoFocus + /> + + {submitting ? ( + + ) : ( + "Create" + )} +
- {creating && ( -
- setDescription(e.target.value)} - disabled={submitting} - /> - -
- )} -
- {getBallots.isLoading ? ( -
Loading ballots...
- ) : ballots.length === 0 ? ( -
No ballots yet.
- ) : null} + )} + + {/* Loading State */} + {getBallots.isLoading && ( +
+ +

Loading ballots...

- {/* Ballot overview table for selected ballot */} -
- {/* Contextual 'Add to ballot' button only on proposal pages - (i.e. when currentProposalId is provided). - - If not on this ballot yet: show Add button - - If already on this ballot: show an informational message */} - {selectedBallot && currentProposalId && ( -
- {!currentProposalAlreadyInSelected ? ( - - ) : ( - - Proposal is on this ballot - - )} -
- )} + )} + + {/* Empty State - No Ballots */} + {!getBallots.isLoading && ballots.length === 0 && ( +
+ +

No ballots yet

+

Create your first ballot to organize proposals for voting

+
+ )} - {selectedBallot ? ( - Array.isArray(selectedBallot.items) && - selectedBallot.items.length > 0 ? ( + {/* Ballot Content */} + {!getBallots.isLoading && selectedBallot && ( + <> + {Array.isArray(selectedBallot.items) && selectedBallot.items.length > 0 ? ( + <> - ) : ( -
No proposals added yet.
- ) - ) : ( -
No proposals added yet.
- )} - {selectedBallot && ( - <> + + {/* Vote Summary */} +
+
+
+ + + {voteSummary.total} {voteSummary.total === 1 ? 'proposal' : 'proposals'} + +
+
+ + Yes: {voteSummary.yes} + + + No: {voteSummary.no} + + + Abstain: {voteSummary.abstain} + +
+
+
+ + {/* Proxy Warning */} {isProxyEnabled && proxies && proxies.length > 0 && !selectedProxyId && ( -
+

Proxy Mode Active - Select a proxy to continue

@@ -674,58 +830,89 @@ export default function BallotCard({

)} - - + + {/* Action Buttons */} +
+ + +
+ ) : ( +
+ +

No proposals in this ballot

+

Add proposals to start voting

+
+ +
+
)} + + )} + + {/* Empty State - No Ballot Selected */} + {!getBallots.isLoading && ballots.length > 0 && !selectedBallot && ( +
+ +

Select a ballot to view proposals

+

Or create a new ballot to get started

-
- {/* Modal to confirm moving proposal between ballots when adding from a proposal page */} + )} + + {/* Confirmation dialog for deleting ballot */} { - if (!open) setMoveModal(null); + if (!open) setDeleteConfirmModal(null); }} > - Move proposal to selected ballot? + Delete Ballot? - {moveModal && ( + {deleteConfirmModal && ( - This proposal is already on ballot{" "} - {moveModal.conflictBallots - .map((b) => b.description || "Untitled ballot") - .join(", ")} - . If you continue, it will be removed from that ballot and - added to the currently selected ballot. + Are you sure you want to delete the ballot "{deleteConfirmModal.ballotDescription}"? + This action cannot be undone and will remove all proposals from this ballot. )} @@ -734,22 +921,216 @@ export default function BallotCard({
- +
+ ); +} + +// Proposal Rationale Editor Component +function ProposalRationaleEditor({ + idx, + rationaleState, + onStateChange, + onUpload, + onLoadFromUrl, + loading, +}: { + idx: number; + rationaleState?: { json: string; url: string; hash: string; loading: boolean; comment?: string }; + onStateChange: (updates: Partial<{ json: string; url: string; hash: string; loading: boolean; comment?: string }>) => void; + onUpload: () => void; + onLoadFromUrl: (url?: string) => void; + loading: boolean; +}) { + const state = rationaleState || { json: "", url: "", hash: "", loading: false, comment: "" }; + + // Construct JSON-LD from comment following CIP-100 structure + const constructJsonLdFromComment = useCallback((comment: string) => { + const jsonLd = { + "@context": { + "CIP100": "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#", + "hashAlgorithm": "CIP100:hashAlgorithm", + "body": { + "@id": "CIP100:body", + "@context": { + "references": { + "@id": "CIP100:references", + "@container": "@set", + "@context": { + "GovernanceMetadata": "CIP100:GovernanceMetadataReference", + "Other": "CIP100:OtherReference", + "label": "CIP100:reference-label", + "uri": "CIP100:reference-uri", + "referenceHash": { + "@id": "CIP100:referenceHash", + "@context": { + "hashDigest": "CIP100:hashDigest", + "hashAlgorithm": "CIP100:hashAlgorithm" + } + } + } + }, + "comment": "CIP100:comment", + "externalUpdates": { + "@id": "CIP100:externalUpdates", + "@context": { + "title": "CIP100:update-title", + "uri": "CIP100:uri" + } + } + } + }, + "authors": { + "@id": "CIP100:authors", + "@container": "@set", + "@context": { + "name": "http://xmlns.com/foaf/0.1/name", + "witness": { + "@id": "CIP100:witness", + "@context": { + "witnessAlgorithm": "CIP100:witnessAlgorithm", + "publicKey": "CIP100:publicKey", + "signature": "CIP100:signature" + } + } + } + } + }, + "authors": [], + "body": { + "comment": comment.trim() + }, + "hashAlgorithm": "blake2b-256" + }; + return JSON.stringify(jsonLd, null, 2); + }, []); + + const handleCommentChange = useCallback((comment: string) => { + if (comment.trim()) { + const jsonLd = constructJsonLdFromComment(comment); + onStateChange({ comment, json: jsonLd }); + } else { + onStateChange({ comment, json: "" }); + } + }, [constructJsonLdFromComment, onStateChange]); + + return ( +
+
+

Voting Rationale (Optional)

+ {state.hash && ( + + Hash: {state.hash.slice(0, 16)}... + + )} +
+
+
+ +