diff --git a/package-lock.json b/package-lock.json index fc2a4d2a..c45b402c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "dompurify": "^3.2.4", + "ethers": "^6.12.0", "framer-motion": "^12.23.0", "graphql": "^16.8.0", "graphql-ws": "^5.14.0", @@ -6238,9 +6239,12 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -9465,6 +9469,11 @@ "pkcs7": "^1.0.4" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -10831,6 +10840,14 @@ "lodash.clonedeep": "^4.5.0" } }, + "node_modules/chessify-protocol/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/chessify-protocol/node_modules/next": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", @@ -12821,6 +12838,98 @@ "url": "https://github.com/bgub/eta?sponsor=1" } }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -16625,6 +16734,14 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -26840,9 +26957,12 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "optional": true, + "peer": true, "requires": { "tslib": "^2.8.0" } @@ -28910,6 +29030,11 @@ "pkcs7": "^1.0.4" } }, + "aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -29866,6 +29991,14 @@ "lodash.clonedeep": "^4.5.0" } }, + "@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "requires": { + "tslib": "^2.8.0" + } + }, "next": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", @@ -31295,6 +31428,64 @@ "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==" }, + "ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "requires": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "dependencies": { + "@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "requires": { + "@noble/hashes": "1.3.2" + } + }, + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } + }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + } + } + }, "eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -33863,6 +34054,7 @@ "eslint-config-prettier": "^8.10.2", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-unused-imports": "^4.4.1", + "ethers": "^6.12.0", "fast-check": "^3.22.0", "framer-motion": "^12.23.0", "graphql": "^16.8.0", @@ -37745,9 +37937,12 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "optional": true, + "peer": true, "requires": { "tslib": "^2.8.0" } @@ -39815,6 +40010,11 @@ "pkcs7": "^1.0.4" } }, + "aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -40771,6 +40971,14 @@ "lodash.clonedeep": "^4.5.0" } }, + "@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "requires": { + "tslib": "^2.8.0" + } + }, "next": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", @@ -42200,6 +42408,64 @@ "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==" }, + "ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "requires": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "dependencies": { + "@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "requires": { + "@noble/hashes": "1.3.2" + } + }, + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } + }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + } + } + }, "eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -44783,6 +45049,14 @@ "styled-jsx": "5.1.6" }, "dependencies": { + "@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "requires": { + "tslib": "^2.8.0" + } + }, "postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -49188,6 +49462,14 @@ "styled-jsx": "5.1.6" }, "dependencies": { + "@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "requires": { + "tslib": "^2.8.0" + } + }, "postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 57eec3c0..0443c5d5 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -1,6 +1,9 @@ 'use client'; import { useMemo, useState } from 'react'; +import { PollCreationModal, type PollDraft } from '@/components/polls/PollCreationModal'; +import { useSettingsStore } from '@/lib/settings/store'; +import { useToast } from '@/context/ToastContext'; import { useTheme } from '@/lib/theme-provider'; import { type ShortcutActionId, @@ -84,6 +87,10 @@ export function CommandPalette() { const [showHelp, setShowHelp] = useState(false); const [query, setQuery] = useState(''); const { theme, setTheme } = useTheme(); + const [pollModalOpen, setPollModalOpen] = useState(false); + + const settings = useSettingsStore((s) => s.settings); + const { info: toastInfo } = useToast(); const commands = useMemo(() => { return [ @@ -129,6 +136,18 @@ export function CommandPalette() { description: 'Focus first available search input', run: () => findSearchInput()?.focus(), }, + { + id: 'openPollCreation', + title: 'Create poll', + description: 'Open poll creation dialog', + run: () => { + if (!settings.pollCreationEnabled) { + toastInfo('Poll creation is disabled in your settings.'); + return; + } + setPollModalOpen(true); + }, + }, { id: 'openShortcutHelp', title: 'Show keyboard shortcuts', @@ -295,6 +314,17 @@ export function CommandPalette() { ) : null} + + setPollModalOpen(false)} + onCreate={(draft: PollDraft) => { + // TODO: integrate with poll creation backend/GraphQL. + // For now, keep placeholder to satisfy typing and modal behavior. + // eslint-disable-next-line no-console + console.log('Create poll draft', draft); + }} + /> ); } diff --git a/src/components/polls/PollCreationModal.tsx b/src/components/polls/PollCreationModal.tsx new file mode 100644 index 00000000..0d7859d0 --- /dev/null +++ b/src/components/polls/PollCreationModal.tsx @@ -0,0 +1,296 @@ +'use client'; + +import React, { useEffect, useMemo, useState } from 'react'; +import { Modal } from '@/components/ui/Modal'; +import { useSettingsStore } from '@/lib/settings/store'; + +export type PollResultsVisibility = 'always' | 'after_voting' | 'after_ended'; + +export type PollDraft = { + question: string; + options: string[]; + durationDays: number; + allowAnonymous: boolean; + resultsVisibility: PollResultsVisibility; +}; + +function validateDraft( + draft: PollDraft, +): { ok: true; value: PollDraft } | { ok: false; message: string } { + const question = draft.question.trim(); + if (question.length < 3) return { ok: false, message: 'Question must be at least 3 characters' }; + if (question.length > 240) return { ok: false, message: 'Question is too long' }; + + const normalizedOptions = draft.options.map((o) => o.trim()); + const nonEmpty = normalizedOptions.filter(Boolean); + if (nonEmpty.length < 2) return { ok: false, message: 'At least two options are required' }; + if (nonEmpty.length > 10) return { ok: false, message: 'At most 10 options are allowed' }; + + for (const opt of nonEmpty) { + if (opt.length > 120) return { ok: false, message: 'Option too long' }; + } + + const durationDays = Number.isFinite(draft.durationDays) + ? Math.trunc(draft.durationDays) + : 0; + if (durationDays < 1 || durationDays > 30) { + return { ok: false, message: 'Duration must be between 1 and 30 days' }; + } + + const resultsVisibility = draft.resultsVisibility; + if (!['always', 'after_voting', 'after_ended'].includes(resultsVisibility)) { + return { ok: false, message: 'Invalid results visibility' }; + } + + return { + ok: true, + value: { + question, + options: normalizedOptions.slice(0, 10), + durationDays, + allowAnonymous: !!draft.allowAnonymous, + resultsVisibility, + }, + }; +} + +export interface PollCreationModalProps { + isOpen: boolean; + onClose: () => void; + onCreate: (draft: PollDraft) => void; +} + +export function PollCreationModal({ isOpen, onClose, onCreate }: PollCreationModalProps) { + const settings = useSettingsStore((s: { settings: any }) => s.settings); + + const initialDraft = useMemo(() => { + return { + question: '', + options: ['', ''], + durationDays: settings.defaultPollDuration, + allowAnonymous: settings.allowAnonymousVoting, + resultsVisibility: settings.pollResultsVisibility as PollResultsVisibility, + }; + }, [settings]); + + const [draft, setDraft] = useState(initialDraft); + const [error, setError] = useState(null); + + useEffect(() => { + if (!isOpen) return; + setDraft(initialDraft); + setError(null); + }, [isOpen, initialDraft]); + + const validate = (): PollDraft | null => { + const result = validateDraft(draft); + if (!result.ok) { + setError(result.message); + return null; + } + setError(null); + return result.value; + }; + + const canSubmit = useMemo(() => { + if (draft.question.trim().length < 3) return false; + const nonEmpty = draft.options.map((o: string) => o.trim()).filter(Boolean); + if (nonEmpty.length < 2) return false; + return true; + }, [draft.question, draft.options]); + + const handleSubmit = () => { + const validated = validate(); + if (!validated) return; + onCreate(validated); + onClose(); + }; + + return ( + +
{ + e.preventDefault(); + handleSubmit(); + }} + > +
+ {error ? ( +
+

{error}

+
+ ) : null} + +
+ + ) => { + const nextQuestion = e.target.value; + setDraft((d: PollDraft) => ({ ...d, question: nextQuestion })); + }} + className="mt-1 block w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-950 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm focus:ring-blue-500 focus:border-blue-500" + aria-invalid={error ? 'true' : 'false'} + /> +
+ +
+
+ + + {draft.options.length}/10 + +
+ +
+ {draft.options.map((opt: string, idx: number) => ( +
+ ) => { + const next = [...draft.options]; + next[idx] = e.target.value; + setDraft((d: PollDraft) => ({ ...d, options: next })); + }} + className="flex-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-950 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm focus:ring-blue-500 focus:border-blue-500" + aria-label={`Option ${idx + 1}`} + /> + + {draft.options.length > 2 ? ( + + ) : null} +
+ ))} +
+ +
+ +
+
+ +
+
+ + ) => { + const v = Number.parseInt(e.target.value || '0', 10); + setDraft((d: PollDraft) => ({ + ...d, + durationDays: Number.isFinite(v) ? v : d.durationDays, + })); + }} + className="mt-1 block w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-950 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm focus:ring-blue-500 focus:border-blue-500" + /> +
+ +
+ ) => { + setDraft((d: PollDraft) => ({ ...d, allowAnonymous: e.target.checked })); + }} + className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + +
+
+ +
+ + +
+ +
+ + +
+
+
+ +
+ {canSubmit ? 'Poll ready to create' : 'Complete question and at least two options'} +
+
+ ); +} + diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts index 9b249e93..f9a062e7 100644 --- a/src/hooks/useKeyboardShortcuts.ts +++ b/src/hooks/useKeyboardShortcuts.ts @@ -6,6 +6,7 @@ const STORAGE_KEY = 'teachlink-keyboard-shortcuts-v1'; export type ShortcutActionId = | 'openCommandPalette' + | 'openPollCreation' | 'goHome' | 'goCourses' | 'goDashboard' @@ -39,6 +40,14 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [ defaultBinding: 'mod+k', binding: 'mod+k', }, + { + id: 'openPollCreation', + label: 'Create poll', + description: 'Open poll creation dialog', + category: 'Interface', + defaultBinding: 'mod+shift+p', + binding: 'mod+shift+p', + }, { id: 'goHome', label: 'Go to Home',