merge adapter and quiz-preview

This commit is contained in:
Tamara 2023-10-15 01:54:39 +03:00
commit 32e0a9f73b
110 changed files with 6331 additions and 2800 deletions

453
package-lock.json generated

@ -13,6 +13,7 @@
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-date-pickers": "^6.16.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
@ -23,9 +24,11 @@
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.0.0",
"axios": "^1.5.1",
"dayjs": "^1.11.10",
"emoji-mart": "^5.5.2",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
"immer": "^10.0.3",
"jszip": "^3.10.1",
"notistack": "^3.0.1",
"react": "^18.2.0",
@ -36,6 +39,7 @@
"react-easy-crop": "^5.0.0",
"react-image-crop": "^10.1.5",
"react-image-file-resizer": "^0.4.8",
"react-rnd": "^10.4.1",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1",
"typescript": "^4.4.2",
@ -1775,11 +1779,11 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz",
"integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
"integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
@ -1797,6 +1801,11 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/@babel/template": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
@ -2416,6 +2425,40 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@floating-ui/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
"integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
"dependencies": {
"@floating-ui/utils": "^0.1.3"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
"dependencies": {
"@floating-ui/core": "^1.4.2",
"@floating-ui/utils": "^0.1.3"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz",
"integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==",
"dependencies": {
"@floating-ui/dom": "^1.5.1"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
@ -3393,11 +3436,11 @@
}
},
"node_modules/@mui/types": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.2.tgz",
"integrity": "sha512-siex8cZDtWeC916cXOoUOnEQQejuMYmHtc4hM6VkKVYaBICz3VIiqyiAomRboTQHt2jchxQ5Q5ATlbcDekTxDA==",
"version": "7.2.6",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.6.tgz",
"integrity": "sha512-7sjLQrUmBwufm/M7jw/quNiPK/oor2+pGUQP2CULRcFCArYTq78oJ3D5esTaL0UMkXKJvDqXn6Ike69yAOBQng==",
"peerDependencies": {
"@types/react": "*"
"@types/react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@ -3406,13 +3449,12 @@
}
},
"node_modules/@mui/utils": {
"version": "5.10.16",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.16.tgz",
"integrity": "sha512-3MB/SGsgiiu9Z55CFmAfiONUoR7AAue/H4F6w3mc2LnhFQCsoVvXhioDPcsiRpUMIQr34jDPzGXdCuqWooPCXQ==",
"version": "5.14.13",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.13.tgz",
"integrity": "sha512-2AFpyXWw7uDCIqRu7eU2i/EplZtks5LAMzQvIhC79sPV9IhOZU2qwOWVnPtdctRXiQJOAaXulg+A37pfhEueQw==",
"dependencies": {
"@babel/runtime": "^7.20.1",
"@types/prop-types": "^15.7.5",
"@types/react-is": "^16.7.1 || ^17.0.0",
"@babel/runtime": "^7.23.1",
"@types/prop-types": "^15.7.7",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
@ -3424,7 +3466,117 @@
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-date-pickers": {
"version": "6.16.2",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.16.2.tgz",
"integrity": "sha512-JFrDUeBkiKtfJ0WqwyPBICEP1U+Ujfsily3ZQ/Hv4zAOleG/5769EgS7TOO4cVgnuhtvQ/pqx2gmuCn8/gcC5w==",
"dependencies": {
"@babel/runtime": "^7.23.1",
"@mui/base": "^5.0.0-beta.17",
"@mui/utils": "^5.14.11",
"@types/react-transition-group": "^4.4.7",
"clsx": "^2.0.0",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.8.6",
"@mui/system": "^5.8.0",
"date-fns": "^2.25.0",
"date-fns-jalali": "^2.13.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
"moment": "^2.29.4",
"moment-hijri": "^2.1.2",
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"date-fns": {
"optional": true
},
"date-fns-jalali": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
},
"moment-hijri": {
"optional": true
},
"moment-jalaali": {
"optional": true
}
}
},
"node_modules/@mui/x-date-pickers/node_modules/@mui/base": {
"version": "5.0.0-beta.19",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.19.tgz",
"integrity": "sha512-maNBgAscddyPNzFZQUJDF/puxM27Li+NqSBsr/lAP8TLns2VvWS2SoL3OKFOIoRnAMKGY/Ic6Aot6gCYeQnssA==",
"dependencies": {
"@babel/runtime": "^7.23.1",
"@floating-ui/react-dom": "^2.0.2",
"@mui/types": "^7.2.6",
"@mui/utils": "^5.14.13",
"@popperjs/core": "^2.11.8",
"clsx": "^2.0.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-date-pickers/node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
@ -3545,9 +3697,9 @@
}
},
"node_modules/@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@ -4342,9 +4494,9 @@
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow=="
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
"version": "15.7.8",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz",
"integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ=="
},
"node_modules/@types/q": {
"version": "1.5.5",
@ -4397,14 +4549,6 @@
"@types/react": "*"
}
},
"node_modules/@types/react-is": {
"version": "17.0.3",
"resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz",
"integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-redux": {
"version": "7.1.27",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.27.tgz",
@ -4417,9 +4561,9 @@
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.7.tgz",
"integrity": "sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==",
"dependencies": {
"@types/react": "*"
}
@ -6853,6 +6997,11 @@
"node": ">=10"
}
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -8347,6 +8496,11 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
},
"node_modules/fast-memoize": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
"integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="
},
"node_modules/fastq": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz",
@ -9430,9 +9584,9 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immer": {
"version": "9.0.16",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz",
"integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==",
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
"integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@ -14761,6 +14915,18 @@
"node": ">=0.10.0"
}
},
"node_modules/re-resizable": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
"integrity": "sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==",
"dependencies": {
"fast-memoize": "^2.5.1"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -14893,6 +15059,15 @@
"node": ">=8"
}
},
"node_modules/react-dev-utils/node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/react-dev-utils/node_modules/loader-utils": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
@ -14961,6 +15136,19 @@
"react": "^18.2.0"
}
},
"node_modules/react-draggable": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz",
"integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==",
"dependencies": {
"clsx": "^1.1.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/react-easy-crop": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.2.tgz",
@ -15039,6 +15227,25 @@
"node": ">=0.10.0"
}
},
"node_modules/react-rnd": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.1.tgz",
"integrity": "sha512-0m887AjQZr6p2ADLNnipquqsDq4XJu/uqVqI3zuoGD19tRm6uB83HmZWydtkilNp5EWsOHbLGF4IjWMdd5du8Q==",
"dependencies": {
"re-resizable": "6.9.6",
"react-draggable": "4.4.5",
"tslib": "2.3.1"
},
"peerDependencies": {
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
}
},
"node_modules/react-rnd/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/react-router": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.0.tgz",
@ -19302,11 +19509,18 @@
}
},
"@babel/runtime": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz",
"integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
"integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
"requires": {
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.14.0"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
}
}
},
"@babel/runtime-corejs3": {
@ -19714,6 +19928,36 @@
}
}
},
"@floating-ui/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
"integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
"requires": {
"@floating-ui/utils": "^0.1.3"
}
},
"@floating-ui/dom": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
"integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
"requires": {
"@floating-ui/core": "^1.4.2",
"@floating-ui/utils": "^0.1.3"
}
},
"@floating-ui/react-dom": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz",
"integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==",
"requires": {
"@floating-ui/dom": "^1.5.1"
}
},
"@floating-ui/utils": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
},
"@humanwhocodes/config-array": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
@ -20369,23 +20613,57 @@
}
},
"@mui/types": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.2.tgz",
"integrity": "sha512-siex8cZDtWeC916cXOoUOnEQQejuMYmHtc4hM6VkKVYaBICz3VIiqyiAomRboTQHt2jchxQ5Q5ATlbcDekTxDA==",
"version": "7.2.6",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.6.tgz",
"integrity": "sha512-7sjLQrUmBwufm/M7jw/quNiPK/oor2+pGUQP2CULRcFCArYTq78oJ3D5esTaL0UMkXKJvDqXn6Ike69yAOBQng==",
"requires": {}
},
"@mui/utils": {
"version": "5.10.16",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.10.16.tgz",
"integrity": "sha512-3MB/SGsgiiu9Z55CFmAfiONUoR7AAue/H4F6w3mc2LnhFQCsoVvXhioDPcsiRpUMIQr34jDPzGXdCuqWooPCXQ==",
"version": "5.14.13",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.13.tgz",
"integrity": "sha512-2AFpyXWw7uDCIqRu7eU2i/EplZtks5LAMzQvIhC79sPV9IhOZU2qwOWVnPtdctRXiQJOAaXulg+A37pfhEueQw==",
"requires": {
"@babel/runtime": "^7.20.1",
"@types/prop-types": "^15.7.5",
"@types/react-is": "^16.7.1 || ^17.0.0",
"@babel/runtime": "^7.23.1",
"@types/prop-types": "^15.7.7",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
}
},
"@mui/x-date-pickers": {
"version": "6.16.2",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.16.2.tgz",
"integrity": "sha512-JFrDUeBkiKtfJ0WqwyPBICEP1U+Ujfsily3ZQ/Hv4zAOleG/5769EgS7TOO4cVgnuhtvQ/pqx2gmuCn8/gcC5w==",
"requires": {
"@babel/runtime": "^7.23.1",
"@mui/base": "^5.0.0-beta.17",
"@mui/utils": "^5.14.11",
"@types/react-transition-group": "^4.4.7",
"clsx": "^2.0.0",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"dependencies": {
"@mui/base": {
"version": "5.0.0-beta.19",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.19.tgz",
"integrity": "sha512-maNBgAscddyPNzFZQUJDF/puxM27Li+NqSBsr/lAP8TLns2VvWS2SoL3OKFOIoRnAMKGY/Ic6Aot6gCYeQnssA==",
"requires": {
"@babel/runtime": "^7.23.1",
"@floating-ui/react-dom": "^2.0.2",
"@mui/types": "^7.2.6",
"@mui/utils": "^5.14.13",
"@popperjs/core": "^2.11.8",
"clsx": "^2.0.0",
"prop-types": "^15.8.1"
}
},
"clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
}
}
},
"@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@ -20457,9 +20735,9 @@
}
},
"@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
},
"@react-dnd/asap": {
"version": "5.0.2",
@ -21060,9 +21338,9 @@
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow=="
},
"@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
"version": "15.7.8",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz",
"integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ=="
},
"@types/q": {
"version": "1.5.5",
@ -21114,14 +21392,6 @@
"@types/react": "*"
}
},
"@types/react-is": {
"version": "17.0.3",
"resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz",
"integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==",
"requires": {
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "7.1.27",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.27.tgz",
@ -21134,9 +21404,9 @@
}
},
"@types/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.7.tgz",
"integrity": "sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==",
"requires": {
"@types/react": "*"
}
@ -22933,6 +23203,11 @@
"whatwg-url": "^8.0.0"
}
},
"dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -24050,6 +24325,11 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
},
"fast-memoize": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
"integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="
},
"fastq": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz",
@ -24838,9 +25118,9 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"immer": {
"version": "9.0.16",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz",
"integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ=="
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
"integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A=="
},
"import-fresh": {
"version": "3.3.0",
@ -28507,6 +28787,14 @@
}
}
},
"re-resizable": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
"integrity": "sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==",
"requires": {
"fast-memoize": "^2.5.1"
}
},
"react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -28608,6 +28896,11 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
},
"loader-utils": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
@ -28652,6 +28945,15 @@
"scheduler": "^0.23.0"
}
},
"react-draggable": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz",
"integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==",
"requires": {
"clsx": "^1.1.1",
"prop-types": "^15.8.1"
}
},
"react-easy-crop": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.2.tgz",
@ -28714,6 +29016,23 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
"react-rnd": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.1.tgz",
"integrity": "sha512-0m887AjQZr6p2ADLNnipquqsDq4XJu/uqVqI3zuoGD19tRm6uB83HmZWydtkilNp5EWsOHbLGF4IjWMdd5du8Q==",
"requires": {
"re-resizable": "6.9.6",
"react-draggable": "4.4.5",
"tslib": "2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"react-router": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.0.tgz",

@ -8,6 +8,7 @@
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-date-pickers": "^6.16.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
@ -18,9 +19,11 @@
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.0.0",
"axios": "^1.5.1",
"dayjs": "^1.11.10",
"emoji-mart": "^5.5.2",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
"immer": "^10.0.3",
"jszip": "^3.10.1",
"notistack": "^3.0.1",
"react": "^18.2.0",
@ -31,6 +34,7 @@
"react-easy-crop": "^5.0.0",
"react-image-crop": "^10.1.5",
"react-image-file-resizer": "^0.4.8",
"react-rnd": "^10.4.1",
"react-router-dom": "^6.6.2",
"react-scripts": "5.0.1",
"typescript": "^4.4.2",

@ -0,0 +1,39 @@
import { Box } from "@mui/material";
export default function CalendarIcon() {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
"&:hover path": {
stroke: "#581CA7",
},
"&:active path": {
stroke: "#FB5607",
},
"&:hover rect": {
stroke: "#581CA7",
},
"&:active rect": {
stroke: "#FB5607",
},
}}
>
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="2.5" width="18" height="18" rx="5" stroke="#7E2AEA" strokeWidth="1.5" />
<path d="M1 7.5H19" stroke="#7E2AEA" strokeWidth="1.5" strokeLinejoin="round" />
<path d="M14.5 1L14.5 4" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.5 1L5.5 4" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4.5 11.5H5.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.5 11.5H10.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.5 11.5H15.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4.5 15.5H5.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.5 15.5H10.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.5 15.5H15.5" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
}

@ -1,7 +1,16 @@
export default function CrossedEyeIcon() {
import { FC, SVGProps } from "react";
export const CrossedEyeIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
return (
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#252734" />
<svg
{...props}
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect y="0.8125" width="30" height="30" rx="6" fill="#FFF" />
<path
d="M7.5 7.5625L22.5 24.0625"
stroke="#7E2AEA"
@ -39,4 +48,4 @@ export default function CrossedEyeIcon() {
/>
</svg>
);
}
};

@ -2,11 +2,17 @@ import { Box } from "@mui/material";
import { FC, SVGProps } from "react";
export const OneIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
<svg {...props} height="1em" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="30" height="30" rx="15" fill="#EEE4FC" />
<svg
fill="#7E2AEA"
height="1em"
viewBox="0 0 30 30"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect width="30" height="30" rx="15" />
<path
d="M15.864 19.5C15.7707 19.5 15.6913 19.472 15.626 19.416C15.57 19.3507 15.542 19.2713 15.542 19.178V11.338L13.218 13.13C13.1433 13.186 13.064 13.2093 12.98 13.2C12.896 13.1907 12.826 13.1487 12.77 13.074L12.406 12.612C12.35 12.528 12.3267 12.444 12.336 12.36C12.3547 12.276 12.4013 12.206 12.476 12.15L15.528 9.798C15.5933 9.75133 15.654 9.72333 15.71 9.714C15.766 9.70467 15.8267 9.7 15.892 9.7H16.606C16.6993 9.7 16.774 9.73267 16.83 9.798C16.886 9.854 16.914 9.92867 16.914 10.022V19.178C16.914 19.2713 16.886 19.3507 16.83 19.416C16.774 19.472 16.6993 19.5 16.606 19.5H15.864Z"
fill="#7E2AEA"
fill={props.style?.color || "#7E2AEA"}
/>
</svg>
);

@ -0,0 +1,34 @@
import { Box, SxProps, Theme } from "@mui/material";
interface Props {
color: string;
sx?: SxProps<Theme>;
}
export default function AddAnswer({ color, sx }: Props) {
return (
<Box
sx={{
height: "38px",
width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",
...sx,
}}
>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="32" height="32" rx="8" fill={color} />
<path
d="M15.518 20.612C15.398 20.612 15.296 20.576 15.212 20.504C15.14 20.42 15.104 20.318 15.104 20.198V16.454H11.414C11.294 16.454 11.192 16.418 11.108 16.346C11.036 16.262 11 16.16 11 16.04V15.464C11 15.344 11.036 15.248 11.108 15.176C11.192 15.092 11.294 15.05 11.414 15.05H15.104V11.414C15.104 11.294 15.14 11.198 15.212 11.126C15.296 11.042 15.398 11 15.518 11H16.148C16.268 11 16.364 11.042 16.436 11.126C16.52 11.198 16.562 11.294 16.562 11.414V15.05H20.27C20.39 15.05 20.486 15.092 20.558 15.176C20.642 15.248 20.684 15.344 20.684 15.464V16.04C20.684 16.16 20.642 16.262 20.558 16.346C20.486 16.418 20.39 16.454 20.27 16.454H16.562V20.198C16.562 20.318 16.52 20.42 16.436 20.504C16.364 20.576 16.268 20.612 16.148 20.612H15.518Z"
fill="#7E2AEA"
/>
</svg>
</Box>
);
}

@ -8,8 +8,8 @@ export default function AddEmoji() {
return (
<Box
sx={{
height: "38px",
width: "45px",
height: "40px",
width: "60px",
display: "flex",
alignItems: "center",
justifyContent: "center",
@ -37,24 +37,24 @@ export default function AddEmoji() {
<path
d="M20 31C21.0949 30.9993 22.1837 30.8371 23.2313 30.5187L31 20C31 17.8244 30.3549 15.6977 29.1462 13.8887C27.9375 12.0798 26.2195 10.6699 24.2095 9.83733C22.1995 9.00477 19.9878 8.78693 17.854 9.21137C15.7202 9.6358 13.7602 10.6835 12.2218 12.2218C10.6835 13.7602 9.6358 15.7202 9.21137 17.854C8.78693 19.9878 9.00477 22.1995 9.83733 24.2095C10.6699 26.2195 12.0798 27.9375 13.8887 29.1462C15.6977 30.3549 17.8244 31 20 31Z"
stroke="#7E2AEA"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15.875 19.083C16.6344 19.083 17.25 18.4674 17.25 17.708C17.25 16.9486 16.6344 16.333 15.875 16.333C15.1156 16.333 14.5 16.9486 14.5 17.708C14.5 18.4674 15.1156 19.083 15.875 19.083Z"
d="M15.875 19.084C16.6344 19.084 17.25 18.4684 17.25 17.709C17.25 16.9496 16.6344 16.334 15.875 16.334C15.1156 16.334 14.5 16.9496 14.5 17.709C14.5 18.4684 15.1156 19.084 15.875 19.084Z"
fill="#7E2AEA"
/>
<path
d="M24.125 19.083C24.8844 19.083 25.5 18.4674 25.5 17.708C25.5 16.9486 24.8844 16.333 24.125 16.333C23.3656 16.333 22.75 16.9486 22.75 17.708C22.75 18.4674 23.3656 19.083 24.125 19.083Z"
d="M24.125 19.084C24.8844 19.084 25.5 18.4684 25.5 17.709C25.5 16.9496 24.8844 16.334 24.125 16.334C23.3656 16.334 22.75 16.9496 22.75 17.709C22.75 18.4684 23.3656 19.084 24.125 19.084Z"
fill="#7E2AEA"
/>
<path
d="M24.7677 22.75C24.2831 23.5849 23.5878 24.2778 22.7512 24.7595C21.9147 25.2412 20.9663 25.4947 20.001 25.4947C19.0357 25.4947 18.0874 25.2412 17.2508 24.7595C16.4143 24.2778 15.719 23.5849 15.2344 22.75"
stroke="#7E2AEA"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</Box>

@ -0,0 +1,43 @@
import { Box, SxProps, Theme } from "@mui/material";
interface Props {
color: string;
sx?: SxProps<Theme>;
}
export default function AnswerGroup({ color, sx }: Props) {
return (
<Box
sx={{
height: "38px",
width: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",
...sx,
}}
>
<svg viewBox="0 0 24 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1 20V2C1 1.44772 1.44772 1 2 1H22C22.5523 1 23 1.44772 23 2V20C23 20.5523 22.5523 21 22 21H2C1.44772 21 1 20.5523 1 20Z"
stroke={color}
stroke-width="1.5"
/>
<path
d="M3 6V4.5C3 3.94772 3.44772 3.5 4 3.5H10C10.5523 3.5 11 3.94772 11 4.5V6C11 6.55228 10.5523 7 10 7H4C3.44772 7 3 6.55228 3 6Z"
stroke={color}
stroke-width="1.5"
/>
<path
d="M13 6V4.5C13 3.94772 13.4477 3.5 14 3.5H20C20.5523 3.5 21 3.94772 21 4.5V6C21 6.55228 20.5523 7 20 7H14C13.4477 7 13 6.55228 13 6Z"
stroke={color}
stroke-width="1.5"
/>
<path
d="M3 12.5V11C3 10.4477 3.44772 10 4 10H20C20.5523 10 21 10.4477 21 11V12.5C21 13.0523 20.5523 13.5 20 13.5H4C3.44772 13.5 3 13.0523 3 12.5Z"
stroke={color}
stroke-width="1.5"
/>
</svg>
</Box>
);
}

29
src/constants/base.ts Normal file

@ -0,0 +1,29 @@
import type { QuizQuestionInitial } from "../model/questionTypes/shared";
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionInitial, "id"> = {
title: "",
type: "nonselected",
expanded: false,
required: false,
deleted: false,
deleteTimeoutId: 0,
content: {
hint: {
text: "",
video: "",
},
rule: {
or: true,
show: true,
title: "",
reqs: [
{
id: "",
vars: [],
},
],
},
back: "",
autofill: false,
},
};

16
src/constants/date.ts Normal file

@ -0,0 +1,16 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionDate } from "../model/questionTypes/date";
export const QUIZ_QUESTION_DATE: Omit<QuizQuestionDate, "id"> = {
...QUIZ_QUESTION_BASE,
type: "date",
content: {
...QUIZ_QUESTION_BASE.content,
required: false,
innerNameCheck: false,
innerName: "",
dateRange: false,
time: false,
},
};

23
src/constants/emoji.ts Normal file

@ -0,0 +1,23 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionEmoji } from "../model/questionTypes/emoji";
export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id"> = {
...QUIZ_QUESTION_BASE,
type: "emoji",
content: {
...QUIZ_QUESTION_BASE.content,
multi: false,
own: false,
innerNameCheck: false,
innerName: "",
required: false,
variants: [
{
answer: "",
hints: "",
extendedText: "",
},
],
},
};

15
src/constants/file.ts Normal file

@ -0,0 +1,15 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionFile } from "../model/questionTypes/file";
export const QUIZ_QUESTION_FILE: Omit<QuizQuestionFile, "id"> = {
...QUIZ_QUESTION_BASE,
type: "file",
content: {
...QUIZ_QUESTION_BASE.content,
required: false,
innerNameCheck: false,
innerName: "",
type: "all",
},
};

27
src/constants/images.ts Normal file

@ -0,0 +1,27 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionImages } from "../model/questionTypes/images";
export const QUIZ_QUESTION_IMAGES: Omit<QuizQuestionImages, "id"> = {
...QUIZ_QUESTION_BASE,
type: "images",
content: {
...QUIZ_QUESTION_BASE.content,
own: false,
multi: false,
xy: "1:1",
innerNameCheck: false,
innerName: "",
large: false,
format: "carousel",
required: false,
variants: [
{
answer: "",
hints: "",
extendedText: "",
},
],
largeCheck: false,
},
};

21
src/constants/number.ts Normal file

@ -0,0 +1,21 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionNumber } from "../model/questionTypes/number";
export const QUIZ_QUESTION_NUMBER: Omit<QuizQuestionNumber, "id"> = {
...QUIZ_QUESTION_BASE,
type: "number",
content: {
...QUIZ_QUESTION_BASE.content,
required: false,
innerNameCheck: false,
innerName: "",
range: "1—100",
defaultValue: 0,
step: 1,
steps: 5,
start: 50,
chooseRange: false,
form: "star",
},
};

16
src/constants/page.ts Normal file

@ -0,0 +1,16 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionPage } from "../model/questionTypes/page";
export const QUIZ_QUESTION_PAGE: Omit<QuizQuestionPage, "id"> = {
...QUIZ_QUESTION_BASE,
type: "page",
content: {
...QUIZ_QUESTION_BASE.content,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
video: "",
},
};

19
src/constants/rating.ts Normal file

@ -0,0 +1,19 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionRating } from "../model/questionTypes/rating";
export const QUIZ_QUESTION_RATING: Omit<QuizQuestionRating, "id"> = {
...QUIZ_QUESTION_BASE,
type: "rating",
content: {
...QUIZ_QUESTION_BASE.content,
required: false,
innerNameCheck: false,
innerName: "",
steps: 5,
ratingExpanded: false,
form: "star",
ratingNegativeDescription: "",
ratingPositiveDescription: "",
},
};

17
src/constants/select.ts Normal file

@ -0,0 +1,17 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionSelect } from "../model/questionTypes/select";
export const QUIZ_QUESTION_SELECT: Omit<QuizQuestionSelect, "id"> = {
...QUIZ_QUESTION_BASE,
type: "select",
content: {
...QUIZ_QUESTION_BASE.content,
multi: false,
required: false,
innerNameCheck: false,
innerName: "",
default: "",
variants: [{ answer: "", hints: "", extendedText: "" }],
},
};

16
src/constants/text.ts Normal file

@ -0,0 +1,16 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionText } from "../model/questionTypes/text";
export const QUIZ_QUESTION_TEXT: Omit<QuizQuestionText, "id"> = {
...QUIZ_QUESTION_BASE,
type: "text",
content: {
...QUIZ_QUESTION_BASE.content,
placeholder: "",
innerNameCheck: false,
innerName: "",
required: false,
answerType: "single",
},
};

18
src/constants/variant.ts Normal file

@ -0,0 +1,18 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionVariant } from "../model/questionTypes/variant";
export const QUIZ_QUESTION_VARIANT: Omit<QuizQuestionVariant, "id"> = {
...QUIZ_QUESTION_BASE,
type: "variant",
content: {
...QUIZ_QUESTION_BASE.content,
largeCheck: false,
multi: false,
own: false,
innerNameCheck: false,
required: false,
innerName: "",
variants: [{ answer: "", hints: "", extendedText: "" }],
},
};

18
src/constants/varimg.ts Normal file

@ -0,0 +1,18 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionVarImg } from "../model/questionTypes/varimg";
export const QUIZ_QUESTION_VARIMG: Omit<QuizQuestionVarImg, "id"> = {
...QUIZ_QUESTION_BASE,
type: "varimg",
content: {
...QUIZ_QUESTION_BASE.content,
own: false,
innerNameCheck: false,
innerName: "",
required: false,
variants: [{ answer: "", hints: "", extendedText: "" }],
largeCheck: false,
replText: "",
},
};

@ -6,7 +6,6 @@ import "./index.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import lightTheme from "./utils/themes/light";
import { ThemeProvider } from "@mui/material";
import StartPage from "./pages/startPage/StartPage";
import Main from "./pages/main";
import QuestionsPage from "./pages/Questions/QuestionsPage";
@ -19,12 +18,22 @@ import ContactFormModal from "@ui_kit/ContactForm";
import ImageCrop from "@ui_kit/Modal/ImageCrop";
import Landing from "./pages/Landing/Landing";
import { SnackbarProvider } from 'notistack'
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/ru";
import dayjs from "dayjs";
import { ruRU } from '@mui/x-date-pickers/locales';
dayjs.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
const routeslink: {
path: string;
page: JSX.Element;
header: boolean;
sidebar: boolean;
path: string;
page: JSX.Element;
header: boolean;
sidebar: boolean;
}[] = [
{ path: "/list", page: <MyQuizzesFull />, header: false, sidebar: false },
{ path: "/questions/:quizId", page: <QuestionsPage />, header: true, sidebar: true,},
@ -37,21 +46,23 @@ const routeslink: {
const root = createRoot(document.getElementById("root")!);
root.render(
<DndProvider backend={HTML5Backend}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider>
<ContactFormModal />
<BrowserRouter>
<Routes>
{routeslink.map((e, i) => (
<Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} />
))}
<Route path="quize-setting/:quizId" element={<StartPage />} />
<Route path="crop" element={<ImageCrop />} />
<Route path="/" element={<Landing/>}/>
</Routes>
</BrowserRouter>
</SnackbarProvider>
</ThemeProvider>
</DndProvider>
<DndProvider backend={HTML5Backend}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider>
<ContactFormModal />
<BrowserRouter>
<Routes>
{routeslink.map((e, i) => (
<Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} />
))}
<Route path="setting/:quizId" element={<StartPage />} />
<Route path="crop" element={<ImageCrop />} />
<Route path="/" element={<Landing/>}/>
</Routes>
</BrowserRouter>
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
</DndProvider>
);

@ -0,0 +1,23 @@
import type {
QuizQuestionBase,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionDate extends QuizQuestionBase {
type: "date";
content: {
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
dateRange: boolean;
time: boolean;
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
};
}

@ -0,0 +1,27 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionEmoji extends QuizQuestionBase {
type: "emoji";
content: {
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
variants: QuestionVariant[];
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
};
}

@ -0,0 +1,33 @@
import type {
QuizQuestionBase,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export const uploadFileTypesMap = {
all: "Все типы файлов",
picture: "Изображения",
video: "Видео",
audio: "Аудио",
document: "Документ",
} as const;
export type UploadFileType = keyof typeof uploadFileTypesMap;
export interface QuizQuestionFile extends QuizQuestionBase {
type: "file";
content: {
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Автозаполнение адреса" */
autofill: boolean;
type: UploadFileType;
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
};
}

@ -0,0 +1,35 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionImages extends QuizQuestionBase {
type: "images";
content: {
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Пропорции */
xy: "1:1" | "1:2" | "2:1";
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Большие картинки" */
large: boolean;
/** Форма */
format: "carousel" | "masonry";
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Варианты (картинки) */
variants: QuestionVariant[];
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
largeCheck: boolean;
};
}

@ -0,0 +1,33 @@
import type {
QuizQuestionBase,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionNumber extends QuizQuestionBase {
type: "number";
content: {
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Диапазон */
range: string;
/** Начальное значение */
start: number;
/** Начальное значение */
defaultValue: number;
/** Шаг */
step: number;
steps: number;
/** Чекбокс "Выбор диапазона (два ползунка)" */
chooseRange: boolean;
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
form: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag";
};
}

@ -0,0 +1,22 @@
import type {
QuizQuestionBase,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionPage extends QuizQuestionBase {
type: "page";
content: {
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
text: string;
picture: string;
video: string;
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
};
}

@ -0,0 +1,29 @@
import type {
QuizQuestionBase,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionRating extends QuizQuestionBase {
type: "rating";
content: {
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
steps: number;
ratingExpanded: boolean;
/** Форма иконки */
form: string;
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
/** Позитивное описание рейтинга */
ratingPositiveDescription: string;
/** Негативное описание рейтинга */
ratingNegativeDescription: string;
};
}

@ -0,0 +1,27 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionSelect extends QuizQuestionBase {
type: "select";
content: {
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Поле "Текст в выпадающем списке" */
default: string;
variants: QuestionVariant[];
rule: QuestionBranchingRule;
hint: QuestionHint;
back: string;
autofill: boolean;
};
}

@ -0,0 +1,77 @@
import type { QuizQuestionDate } from "./date";
import type { QuizQuestionEmoji } from "./emoji";
import type { QuizQuestionFile } from "./file";
import type { QuizQuestionImages } from "./images";
import type { QuizQuestionNumber } from "./number";
import type { QuizQuestionPage } from "./page";
import type { QuizQuestionRating } from "./rating";
import type { QuizQuestionSelect } from "./select";
import type { QuizQuestionText } from "./text";
import type { QuizQuestionVariant } from "./variant";
import type { QuizQuestionVarImg } from "./varimg";
export interface QuestionBranchingRule {
/** Радиокнопка "Все условия обязательны" */
or: boolean;
show: boolean;
title: string;
reqs: {
id: string;
/** Список выбранных вариантов */
vars: number[];
}[];
}
export interface QuestionHint {
/** Текст подсказки */
text: string;
/** URL видео подсказки */
video: string;
}
export type QuestionVariant = {
/** Текст */
answer: string;
/** Текст подсказки */
hints: string;
/** Дополнительное поле для текста, emoji, ссылки на картинку */
extendedText: string;
};
export interface QuizQuestionBase {
id: number;
title: string;
type: string;
expanded: boolean;
required: boolean;
deleted: boolean;
deleteTimeoutId: number;
content: {
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
};
}
export interface QuizQuestionInitial extends QuizQuestionBase {
type: "nonselected";
}
export type AnyQuizQuestion =
| QuizQuestionVariant
| QuizQuestionImages
| QuizQuestionVarImg
| QuizQuestionEmoji
| QuizQuestionText
| QuizQuestionSelect
| QuizQuestionDate
| QuizQuestionNumber
| QuizQuestionFile
| QuizQuestionPage
| QuizQuestionRating
| QuizQuestionInitial;
export type QuizQuestionType = AnyQuizQuestion["type"];
export type DefiniteQuestionType = Exclude<QuizQuestionType, "nonselected">;

@ -0,0 +1,24 @@
import type {
QuizQuestionBase,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionText extends QuizQuestionBase {
type: "text";
content: {
placeholder: string;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Автозаполнение адреса" */
autofill: boolean;
answerType: "single" | "multi" | "number";
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
};
}

@ -0,0 +1,30 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionVariant extends QuizQuestionBase {
type: "variant";
content: {
/** Чекбокс "Длинный текстовый ответ" */
largeCheck: boolean;
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Варианты ответов */
variants: QuestionVariant[];
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
};
}

@ -0,0 +1,27 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionVarImg extends QuizQuestionBase {
type: "varimg";
content: {
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
variants: QuestionVariant[];
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
autofill: boolean;
largeCheck: boolean;
replText: string;
};
}

@ -1,5 +1,4 @@
import React from "react";
import Stepper from "@ui_kit/Stepper";
import { Box, Button, IconButton, Typography, Paper, useTheme, Link, SxProps, Theme, TextField } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import CustomTextField from "@ui_kit/CustomTextField";
@ -41,7 +40,6 @@ export default function ContactFormPage() {
const theme = useTheme();
return (
<>
<Stepper activeStep={activeStep} desc={"Настройте форму контактов"} />
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Link
sx={{

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import {
Box,
Button,
@ -18,7 +19,7 @@ import {
Tooltip,
useTheme,
} from "@mui/material";
import Stepper from "@ui_kit/Stepper";
import { quizStore } from "@root/quizes";
import LinkIcon from "../../assets/icons/LinkIcon";
import InfoIcon from "../../assets/icons/InfoIcon";
import ArrowDown from "../../assets/icons/ArrowDownIcon";
@ -70,17 +71,8 @@ export default function InstallQuiz() {
},
];
const [activeStep, setActiveStep] = React.useState(5);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const [display, setDisplay] = React.useState("1");
const quizId = Number(useParams().quizId);
const handleChange = (event: SelectChangeEvent) => {
setDisplay(event.target.value);
};
@ -96,9 +88,14 @@ export default function InstallQuiz() {
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
const theme = useTheme();
const { listQuizes, updateQuizesList } = quizStore();
const handleNext = () => {
updateQuizesList(quizId, { step: listQuizes[quizId].step + 1 });
};
return (
<>
<Stepper activeStep={activeStep} desc={"Установите квиз"} />
<Box sx={{ marginTop: "60px", display: "flex", gap: "40px" }}>
<Paper
sx={{
@ -416,7 +413,7 @@ export default function InstallQuiz() {
>
<ArrowLeft />
</Button>
<Button variant="contained">Запустить рекламу</Button>
<Button variant="contained" onClick={handleNext}>Запустить рекламу</Button>
</Box>
<Modal

@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { Draggable } from "react-beautiful-dnd";
import {
@ -21,13 +21,14 @@ import { MessageIcon } from "@icons/messagIcon";
import TextareaAutosize from "@mui/base/TextareaAutosize";
import type { ChangeEvent, KeyboardEvent, ReactNode } from "react";
import type { Variants } from "@root/questions";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
type AnswerItemProps = {
index: number;
totalIndex: number;
variants: Variants[];
variant: Variants;
variants: QuestionVariant[];
variant: QuestionVariant;
additionalContent?: ReactNode;
additionalMobile?: ReactNode;
icon?: ReactNode;
@ -40,19 +41,19 @@ export const AnswerItem = ({
variant,
additionalContent,
additionalMobile,
icon,
}: AnswerItemProps) => {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme();
const question = listQuestions[quizId][totalIndex] as QuizQuestionVariant;
const isTablet = useMediaQuery(theme.breakpoints.down(790));
const debounced = useDebouncedCallback((value) => {
const answerNew = variants.slice();
answerNew[index].answer = value;
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
...question.content,
variants: answerNew,
},
});
@ -72,13 +73,10 @@ export const AnswerItem = ({
const addNewAnswer = () => {
const answerNew = variants.slice();
answerNew.push({ answer: "", hints: "", emoji: "", image: "" });
answerNew.push({ answer: "", hints: "", extendedText: "" });
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
@ -86,11 +84,8 @@ export const AnswerItem = ({
const answerNew = variants.slice();
answerNew.splice(index, 1);
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
@ -98,11 +93,8 @@ export const AnswerItem = ({
const answerNew = variants.slice();
answerNew[index].hints = event.target.value;
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
@ -126,13 +118,10 @@ export const AnswerItem = ({
fullWidth
focused={false}
placeholder={"Добавьте ответ"}
multiline={listQuestions[quizId][totalIndex].content.largeCheck}
multiline={question.content.largeCheck}
onChange={({ target }) => debounced(target.value)}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (
event.code === "Enter" &&
!listQuestions[quizId][totalIndex].content.largeCheck
) {
if (event.code === "Enter" && !question.content.largeCheck) {
addNewAnswer();
}
}}
@ -166,13 +155,11 @@ export const AnswerItem = ({
/>
</IconButton>
<Popover
id="my-popover-id"
open={isOpen}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
>
<TextareaAutosize
style={{ margin: "10px" }}
@ -197,10 +184,15 @@ export const AnswerItem = ({
}}
sx={{
"& .MuiInputBase-root": {
padding: icon ? "5px 13.5px" : "13.5px 13.5px 13.5px 9px",
borderRadius: "8px",
height: "48px",
padding: additionalContent ? "5px 13px" : "13px",
borderRadius: "10px",
background: "#ffffff",
"& input.MuiInputBase-input": {
height: "22px",
},
"& textarea.MuiInputBase-input": {
marginTop: "1px",
},
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},

@ -10,13 +10,13 @@ import { reorder } from "./helper";
import type { ReactNode } from "react";
import type { DropResult } from "react-beautiful-dnd";
import type { Variants } from "@root/questions";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
type AnswerDraggableListProps = {
variants: Variants[];
variants: QuestionVariant[];
totalIndex: number;
additionalContent?: (variant: Variants, index: number) => ReactNode;
additionalMobile?: (variant: Variants, index: number) => ReactNode;
additionalContent?: (variant: QuestionVariant, index: number) => ReactNode;
additionalMobile?: (variant: QuestionVariant, index: number) => ReactNode;
};
export const AnswerDraggableList = ({

@ -4,20 +4,39 @@ import { useParams } from "react-router-dom";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import Clue from "../../assets/icons/questionsPage/clue";
import Branching from "../../assets/icons/questionsPage/branching";
import { Box, Typography, Tooltip, IconButton, useTheme, useMediaQuery } from "@mui/material";
import {
Box,
Typography,
Tooltip,
IconButton,
useTheme,
useMediaQuery,
} from "@mui/material";
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import { questionStore, resetSomeField, copyQuestion, removeQuestionForce, updateQuestionsList, removeQuestion } from "@root/questions";
import {
questionStore,
resetSomeField,
copyQuestion,
removeQuestionForce,
updateQuestionsList,
removeQuestion,
} from "@root/questions";
import { quizStore } from "@root/quizes";
import { DoubleTick } from "@icons/questionsPage/DoubleTick";
import { DoubleArrowRight } from "@icons/questionsPage/DoubleArrowRight";
import { VectorQuestions } from "@icons/questionsPage/VectorQuestions";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import type { SxProps } from "@mui/material";
import type { QuizQuestionBase } from "../../model/questionTypes/shared";
interface Props {
switchState: string;
SSHC: (data: string) => void;
totalIndex: number;
sx?: SxProps;
}
export default function ButtonsOptions({
@ -27,12 +46,15 @@ export default function ButtonsOptions({
}: Props) {
const quizId = Number(useParams().quizId);
const { openedModalSettings, listQuestions } = questionStore();
const { listQuizes } = quizStore();
const [openedReallyChangingModal, setOpenedReallyChangingModal] =
useState<boolean>(false);
const quize = listQuizes[quizId];
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
useEffect(() => {
if (listQuestions[quizId][totalIndex].deleteTimeoutId) {
clearTimeout(listQuestions[quizId][totalIndex].deleteTimeoutId);
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
}, [listQuestions]);
@ -110,6 +132,19 @@ export default function ButtonsOptions({
<Tooltip
arrow
placement="right"
componentsProps={{
tooltip: {
sx: {
background: "#fff",
borderRadius: "6px",
color: "#9A9AAF",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
"& .MuiTooltip-arrow": {
color: "#FFF",
},
},
},
}}
title={
<Box>
<Typography
@ -157,69 +192,80 @@ export default function ButtonsOptions({
: theme.palette.grey3.main,
minWidth: isWrappMiniButtonSetting ? "30px" : "64px",
height: "30px",
"&:hover": {
color: theme.palette.grey3.main,
"& path": { stroke: theme.palette.grey3.main },
},
}}
>
{icon}
{isWrappMiniButtonSetting ? null : title}
</MiniButtonSetting>
</Tooltip>
) : (
<MiniButtonSetting
key={title}
onClick={() => {
SSHC(value);
myFunc();
}}
sx={{
backgroundColor:
switchState === value
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === value
? "#ffffff"
: theme.palette.grey3.main,
minWidth: isWrappMiniButtonSetting ? "30px" : "64px",
height: "30px",
}}
>
{icon}
{isWrappMiniButtonSetting ? null : title}
</MiniButtonSetting>
<>
<MiniButtonSetting
key={title}
onClick={() => {
SSHC(value);
myFunc();
}}
sx={{
backgroundColor:
switchState === value
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === value
? "#ffffff"
: theme.palette.grey3.main,
minWidth: isWrappMiniButtonSetting ? "30px" : "64px",
height: "30px",
"&:hover": {
color: theme.palette.grey3.main,
"& path": { stroke: theme.palette.grey3.main },
},
}}
>
{icon}
{isWrappMiniButtonSetting ? null : title}
</MiniButtonSetting>
</>
)}
</Box>
))}
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
</>
</Box>
<Box
sx={{
@ -239,9 +285,9 @@ export default function ButtonsOptions({
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
const removedId = listQuestions[quizId][totalIndex].id;
if (listQuestions[quizId][totalIndex].deleteTimeoutId) {
clearTimeout(listQuestions[quizId][totalIndex].deleteTimeoutId);
const removedId = question.id;
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
removeQuestion(quizId, totalIndex);
@ -250,8 +296,8 @@ export default function ButtonsOptions({
removeQuestionForce(quizId, removedId);
}, 5000);
updateQuestionsList(quizId, totalIndex, {
...listQuestions[quizId][totalIndex],
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
...question,
deleteTimeoutId: newTimeoutId,
});
}}
@ -261,4 +307,4 @@ export default function ButtonsOptions({
</Box>
</Box>
);
}
}

@ -1,10 +0,0 @@
.MuiTooltip-popper > .MuiTooltip-tooltip {
background: #fff;
border-radius: 6px;
color: #9A9AAF;
box-shadow: 0px 8px 24px rgba(210, 208, 225, 0.4);
}
.MuiTooltip-popper > .MuiTooltip-tooltip .MuiTooltip-arrow::before {
background: #fff;
}

@ -3,12 +3,20 @@ import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import Clue from "../../assets/icons/questionsPage/clue";
import Branching from "../../assets/icons/questionsPage/branching";
import { Box, IconButton, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
Box,
IconButton,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import { DeleteIcon } from "../../assets/icons/questionsPage/deleteIcon";
import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
import { useParams } from "react-router-dom";
import { quizStore } from "@root/quizes";
import {
questionStore,
copyQuestion,
@ -22,7 +30,7 @@ import { DoubleTick } from "@icons/questionsPage/DoubleTick";
import { VectorQuestions } from "@icons/questionsPage/VectorQuestions";
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
import "./ButtonsOptionsAndPict.css";
import type { QuizQuestionBase } from "../../model/questionTypes/shared";
interface Props {
switchState: string;
@ -30,19 +38,26 @@ interface Props {
totalIndex: number;
}
export default function ButtonsOptionsAndPict({ SSHC, switchState, totalIndex }: Props) {
export default function ButtonsOptionsAndPict({
SSHC,
switchState,
totalIndex,
}: Props) {
const [buttonHover, setButtonHover] = useState<string>("");
const quizId = Number(useParams().quizId);
const { listQuizes } = quizStore();
const { listQuestions } = questionStore();
const [openedReallyChangingModal, setOpenedReallyChangingModal] =
useState<boolean>(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const quize = listQuizes[quizId];
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
useEffect(() => {
if (listQuestions[quizId][totalIndex].deleteTimeoutId) {
clearTimeout(listQuestions[quizId][totalIndex].deleteTimeoutId);
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
}, [listQuestions]);
@ -79,10 +94,15 @@ export default function ButtonsOptionsAndPict({ SSHC, switchState, totalIndex }:
maxWidth: "104px",
minWidth: isIconMobile ? "30px" : "64px",
height: "30px",
backgroundColor: switchState === "setting" ? theme.palette.brightPurple.main : "transparent",
color: switchState === "setting" ? "#ffffff" : theme.palette.grey3.main,
backgroundColor:
switchState === "setting"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "setting" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color: switchState === "setting" ? theme.palette.grey3.main : null,
color:
switchState === "setting" ? theme.palette.grey3.main : null,
},
}}
>
@ -107,8 +127,12 @@ export default function ButtonsOptionsAndPict({ SSHC, switchState, totalIndex }:
minWidth: isIconMobile ? "30px" : "64px",
maxWidth: "102px",
height: "30px",
backgroundColor: switchState === "help" ? theme.palette.brightPurple.main : "transparent",
color: switchState === "help" ? "#ffffff" : theme.palette.grey3.main,
backgroundColor:
switchState === "help"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "help" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color: switchState === "help" ? theme.palette.grey3.main : null,
},
@ -125,123 +149,156 @@ export default function ButtonsOptionsAndPict({ SSHC, switchState, totalIndex }:
/>
{isIconMobile ? null : "Подсказка"}
</MiniButtonSetting>
<Tooltip
arrow
placement="right"
title={
<Box>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: "bold",
fontSize: "14px",
marginBottom: "10px",
}}
>
Будет показан при условии
</Typography>
<Typography sx={{ fontWeight: "bold", fontSize: "12px" }}>Название</Typography>
<Typography
sx={{
fontWeight: "bold",
fontSize: "12px",
marginBottom: "10px",
}}
>
Условие 1, Условие 2
</Typography>
<Typography sx={{ color: "#7E2AEA", fontSize: "12px" }}>Все условия обязательны</Typography>
</Box>
}
>
<>
<Tooltip
arrow
placement="right"
componentsProps={{
tooltip: {
sx: {
background: "#fff",
borderRadius: "6px",
color: "#9A9AAF",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
"& .MuiTooltip-arrow": {
color: "#FFF",
},
},
},
}}
title={
<Box>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: "bold",
fontSize: "14px",
marginBottom: "10px",
}}
>
Будет показан при условии
</Typography>
<Typography sx={{ fontWeight: "bold", fontSize: "12px" }}>
Название
</Typography>
<Typography
sx={{
fontWeight: "bold",
fontSize: "12px",
marginBottom: "10px",
}}
>
Условие 1, Условие 2
</Typography>
<Typography sx={{ color: "#7E2AEA", fontSize: "12px" }}>
Все условия обязательны
</Typography>
</Box>
}
>
<MiniButtonSetting
onMouseEnter={() => setButtonHover("branching")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
SSHC("branching");
openedModal();
}}
sx={{
height: "30px",
maxWidth: "103px",
minWidth: isIconMobile ? "30px" : "64px",
backgroundColor:
switchState === "branching"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "branching"
? "#ffffff"
: theme.palette.grey3.main,
"&:hover": {
color:
switchState === "branching"
? theme.palette.grey3.main
: null,
},
}}
>
<Branching
color={
buttonHover === "branching"
? theme.palette.grey3.main
: switchState === "branching"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Ветвление"}
</MiniButtonSetting>
</Tooltip>
<MiniButtonSetting
onMouseEnter={() => setButtonHover("branching")}
onMouseEnter={() => setButtonHover("image")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
SSHC("branching");
openedModal();
SSHC("image");
}}
sx={{
height: "30px",
maxWidth: "103px",
maxWidth: "123px",
minWidth: isIconMobile ? "30px" : "64px",
backgroundColor: switchState === "branching" ? theme.palette.brightPurple.main : "transparent",
color: switchState === "branching" ? "#ffffff" : theme.palette.grey3.main,
backgroundColor:
switchState === "image"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "image" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color: switchState === "branching" ? theme.palette.grey3.main : null,
color:
switchState === "image" ? theme.palette.grey3.main : null,
},
}}
>
<Branching
<ImgIcon
color={
buttonHover === "branching"
buttonHover === "image"
? theme.palette.grey3.main
: switchState === "branching"
: switchState === "image"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Ветвление"}
{isIconMobile ? null : "Изображение"}
</MiniButtonSetting>
</Tooltip>
<MiniButtonSetting
onMouseEnter={() => setButtonHover("image")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
SSHC("image");
}}
sx={{
height: "30px",
maxWidth: "123px",
minWidth: isIconMobile ? "30px" : "64px",
backgroundColor: switchState === "image" ? theme.palette.brightPurple.main : "transparent",
color: switchState === "image" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color: switchState === "image" ? theme.palette.grey3.main : null,
},
}}
>
<ImgIcon
color={
buttonHover === "image"
? theme.palette.grey3.main
: switchState === "image"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Изображение"}
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
<MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
height: "30px",
backgroundColor: "#FEDFD0",
}}
>
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
</>
</Box>
<Box
sx={{
@ -251,21 +308,18 @@ export default function ButtonsOptionsAndPict({ SSHC, switchState, totalIndex }:
<IconButton sx={{ borderRadius: "6px", padding: "0px 2px" }}>
<HideIcon style={{ color: "#4D4D4D" }} />
</IconButton>
<IconButton sx={{ borderRadius: "6px" }} onClick={() => copyQuestion(quizId, totalIndex)}>
<CopyIcon style={{ color: "#4D4D4D" }} />
</IconButton>
<IconButton
sx={{ borderRadius: "6px", padding: " 0px 2px" }}
sx={{ borderRadius: "6px" }}
onClick={() => copyQuestion(quizId, totalIndex)}
>
<CopyIcon color={"#4D4D4D"} />
<CopyIcon style={{ color: "#4D4D4D" }} />
</IconButton>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
const removedId = listQuestions[quizId][totalIndex].id;
if (listQuestions[quizId][totalIndex].deleteTimeoutId) {
clearTimeout(listQuestions[quizId][totalIndex].deleteTimeoutId);
const removedId = question.id;
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
removeQuestion(quizId, totalIndex);
@ -274,8 +328,8 @@ export default function ButtonsOptionsAndPict({ SSHC, switchState, totalIndex }:
removeQuestionForce(quizId, removedId);
}, 5000);
updateQuestionsList(quizId, totalIndex, {
...listQuestions[quizId][totalIndex],
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
...question,
deleteTimeoutId: newTimeoutId,
});
}}

@ -1,11 +1,9 @@
import { useParams } from "react-router-dom";
import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material";
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import InfoIcon from "../../../assets/icons/InfoIcon";
import ButtonsOptions from "../ButtonsOptions";
import SwitchData from "./switchData";
import { useState, useEffect } from "react";
import InfoIcon from "../../../assets/icons/InfoIcon";
import SelectableButton from "@ui_kit/SelectableButton";
import { questionStore, updateQuestionsList } from "@root/questions";
interface Props {
totalIndex: number;
@ -13,8 +11,6 @@ interface Props {
export default function DataOptions({ totalIndex }: Props) {
const [switchState, setSwitchState] = useState("setting");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -22,14 +18,6 @@ export default function DataOptions({ totalIndex }: Props) {
setSwitchState(data);
};
useEffect(() => {
if (listQuestions[quizId][totalIndex].content.type !== "mask") {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.type = "calendar";
updateQuestionsList(quizId, totalIndex, { content: clonContent });
}
}, []);
return (
<>
<Box
@ -43,31 +31,7 @@ export default function DataOptions({ totalIndex }: Props) {
gap: isMobile ? "18px" : "20px",
}}
>
<Box sx={{ gap: "10px", display: "flex", flexWrap: "wrap" }}>
<SelectableButton
isSelected={listQuestions[quizId][totalIndex].content.type === "calendar"}
onClick={() => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.type = "calendar";
updateQuestionsList(quizId, totalIndex, { content: clonContent });
}}
sx={{ maxWidth: "257px", height: "48px", whiteSpace: "nowrap" }}
>
Использовать календарь
</SelectableButton>
<SelectableButton
isSelected={listQuestions[quizId][totalIndex].content.type === "mask"}
onClick={() => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.type = "mask";
updateQuestionsList(quizId, totalIndex, { content: clonContent });
}}
sx={{ maxWidth: "211px", height: "48px", whiteSpace: "nowrap" }}
>
Использовать маску
</SelectableButton>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mb: "20px" }}>
<Box sx={{ display: "flex", alignItems: "flex-start", gap: "12px", mb: "20px" }}>
<Typography
sx={{
fontWeight: 400,

@ -6,6 +6,8 @@ import CustomTextField from "@ui_kit/CustomTextField";
import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
type SettingsDataProps = {
totalIndex: number;
};
@ -16,12 +18,13 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionDate;
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
return (
@ -34,7 +37,7 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
>
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pt: "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
display: "flex",
@ -49,21 +52,21 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Выбор диапазона дат"}
checked={listQuestions[quizId][totalIndex].content.dateRange}
checked={question.content.dateRange}
handleChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.dateRange = target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, {
content: { ...question.content, dateRange: target.checked },
});
}}
/>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Выбор времени"}
checked={listQuestions[quizId][totalIndex].content.time}
checked={question.content.time}
handleChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.time = target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, {
content: { ...question.content, time: target.checked },
});
}}
/>
</Box>
@ -76,7 +79,7 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
width: "100%",
width: isMobile ? "auto" : "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
@ -85,27 +88,36 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
required: !e.target.checked,
checked={!question.required}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, {
required: !target.checked,
});
}}
/>
<Box sx={{ width: isMobile ? "93%" : "auto", display: "flex", alignItems: "center" }}>
<Box
sx={{
width: isMobile ? "93%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
sx={{
mr: isMobile ? "0px" : "16px",
height: isMobile ? "100%" : "26px",
alignItems: isMobile ? "flex-start" : "center",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
@ -114,10 +126,10 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -17,11 +17,16 @@ import {
import {
questionStore,
updateQuestionsList,
DEFAULT_QUESTION,
removeQuestionForce,
createQuestion,
} from "@root/questions";
import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions";
import type { RefObject } from "react";
import type {
QuizQuestionType,
QuizQuestionBase,
} from "../../../model/questionTypes/shared";
type ChooseAnswerModalProps = {
open: boolean;
@ -39,7 +44,7 @@ export const ChooseAnswerModal = ({
switchState,
}: ChooseAnswerModalProps) => {
const [openModal, setOpenModal] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<string>("");
const [selectedValue, setSelectedValue] = useState<QuizQuestionType>("text");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme();
@ -124,11 +129,12 @@ export const ChooseAnswerModal = ({
onClick={() => {
setOpenModal(false);
const question = listQuestions[quizId][totalIndex];
updateQuestionsList(quizId, totalIndex, {
...DEFAULT_QUESTION,
const question = { ...listQuestions[quizId][totalIndex] };
removeQuestionForce(quizId, question.id);
createQuestion(quizId, selectedValue, totalIndex);
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
expanded: question.expanded,
type: selectedValue,
});
}}
>

@ -5,84 +5,82 @@ import { Box, ListItem, Typography, useTheme } from "@mui/material";
import QuestionsPageCard from "./QuestionPageCard";
import { questionStore, updateQuestionsList } from "@root/questions";
import { updateQuestionsList } from "@root/questions";
import {Question} from "@root/questions"
import { QuizQuestionBase } from "../../../model/questionTypes/shared";
type DraggableListItemProps = {
index: number;
isDragging: boolean;
questionData: Question;
questionData: QuizQuestionBase;
};
export default memo(({
index,
isDragging,
questionData
}: DraggableListItemProps) => {
const quizId = Number(useParams().quizId);
const theme = useTheme();
console.log("Мой индекс "+index)
console.log(questionData)
export default memo(
({ index, isDragging, questionData }: DraggableListItemProps) => {
const quizId = Number(useParams().quizId);
const theme = useTheme();
console.log("Мой индекс " + index);
console.log(questionData);
return (
<Draggable draggableId={String(index)} index={index}>
{(provided) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
sx={{ userSelect: "none", padding: 0 }}
>
{questionData.deleted ? (
<Box
{...provided.dragHandleProps}
sx={{
width: "100%",
maxWidth: "800px",
display: "flex",
justifyContent: "center",
marginBottom: "40px",
gap: "5px",
}}
>
<Typography
return (
<Draggable draggableId={String(index)} index={index}>
{(provided) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
sx={{ userSelect: "none", padding: 0 }}
>
{questionData.deleted ? (
<Box
{...provided.dragHandleProps}
sx={{
fontSize: "16px",
color: theme.palette.grey2.main,
width: "100%",
maxWidth: "800px",
display: "flex",
justifyContent: "center",
marginBottom: "40px",
gap: "5px",
}}
>
Вопрос удалён.
</Typography>
<Typography
onClick={() => {
updateQuestionsList(quizId, index, {
...questionData,
deleted: false,
});
}}
sx={{
cursor: "pointer",
fontSize: "16px",
textDecoration: "underline",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
>
Восстановить?
</Typography>
</Box>
) : (
<Box sx={{ width: "100%", position: "relative" }}>
<QuestionsPageCard
key={index}
totalIndex={index}
draggableProps={provided.dragHandleProps}
isDragging={isDragging}
/>
</Box>
)}
</ListItem>
)}
</Draggable>
);
});
<Typography
sx={{
fontSize: "16px",
color: theme.palette.grey2.main,
}}
>
Вопрос удалён.
</Typography>
<Typography
onClick={() => {
updateQuestionsList<QuizQuestionBase>(quizId, index, {
...questionData,
deleted: false,
});
}}
sx={{
cursor: "pointer",
fontSize: "16px",
textDecoration: "underline",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
>
Восстановить?
</Typography>
</Box>
) : (
<Box sx={{ width: "100%", position: "relative" }}>
<QuestionsPageCard
key={index}
totalIndex={index}
draggableProps={provided.dragHandleProps}
isDragging={isDragging}
/>
</Box>
)}
</ListItem>
)}
</Draggable>
);
}
);

@ -32,7 +32,7 @@ import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { OneIcon } from "@icons/questionsPage/OneIcon";
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
import CrossedEyeIcon from "@icons/CrossedEyeIcon";
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
import { HideIcon } from "@icons/questionsPage/hideIcon";
import Answer from "@icons/questionsPage/answer";
import OptionsPict from "@icons/questionsPage/options_pict";
@ -45,10 +45,11 @@ import Slider from "@icons/questionsPage/slider";
import Download from "@icons/questionsPage/download";
import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating";
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
import type { AnyQuizQuestion, QuizQuestionInitial } from "../../../model/questionTypes/shared";
interface Props {
totalIndex: number;
@ -59,32 +60,91 @@ interface Props {
const IconAndrom = (isExpanded: boolean, switchState: string) => {
switch (switchState) {
case "variant":
return <Answer color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<Answer
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "images":
return <OptionsPict color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<OptionsPict
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "varimg":
return <OptionsAndPict color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<OptionsAndPict
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "emoji":
return <Emoji color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<Emoji
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "text":
return <Input color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<Input
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "select":
return <DropDown color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<DropDown
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "date":
return <Date color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<Date
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "number":
return <Slider color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<Slider
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "file":
return <Download color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<Download
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "page":
return <Page color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<Page
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "rating":
return <RatingIcon color={isExpanded ? "#9A9AAF" : "white"} sx={{ height: "22px", width: "20px" }} />;
return (
<RatingIcon
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
default:
return <></>;
}
};
export default function QuestionsPageCard({ totalIndex, draggableProps, isDragging }: Props) {
export default function QuestionsPageCard({
totalIndex,
draggableProps,
isDragging,
}: Props) {
const [plusVisible, setPlusVisible] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false);
const quizId = Number(useParams().quizId);
@ -92,17 +152,17 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const { listQuestions } = questionStore();
const { type: switchState, expanded: isExpanded } = listQuestions[quizId][totalIndex];
const question = listQuestions[quizId][totalIndex];
const anchorRef = useRef(null);
const debounced = useDebouncedCallback((title) => {
updateQuestionsList(quizId, totalIndex, { title });
}, 1000);
updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, { title });
}, 200);
useEffect(() => {
if (listQuestions[quizId][totalIndex].deleteTimeoutId) {
clearTimeout(listQuestions[quizId][totalIndex].deleteTimeoutId);
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
}, [listQuestions]);
}, [question]);
return (
<>
@ -111,17 +171,18 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
sx={{
maxWidth: "796px",
width: "100%",
borderRadius: "8px",
backgroundColor: isExpanded ? "white" : "#333647",
borderRadius: "12px",
backgroundColor: question.expanded ? "white" : "#EEE4FC",
border: question.expanded ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: isTablet ? (isMobile ? "20px 20px 0px 20px" : "20px 10px 20px 20px ") : "20px 10px 20px 20px",
flexDirection: isMobile ? "column-reverse" : null,
padding: isMobile ? "10px" : "20px 10px 20px 20px",
flexDirection: isMobile ? "column" : null,
}}
>
<FormControl
@ -134,7 +195,7 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
}}
>
<TextField
defaultValue={listQuestions[quizId][totalIndex].title}
defaultValue={question.title}
placeholder={"Заголовок вопроса"}
onChange={({ target }) => debounced(target.value)}
InputProps={{
@ -146,26 +207,34 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
sx={{ cursor: "pointer" }}
onClick={() => setOpen((isOpened) => !isOpened)}
>
{IconAndrom(isExpanded, switchState)}
{IconAndrom(question.expanded, question.type)}
</InputAdornment>
<ChooseAnswerModal
open={open}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
totalIndex={totalIndex}
switchState={switchState}
switchState={question.type}
/>
</Box>
),
}}
sx={{
margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
color: isExpanded ? "#9A9AAF" : "white",
backgroundColor: isExpanded ? theme.palette.background.default : "transparent",
color: question.expanded ? "#9A9AAF" : "#4D4D4D",
backgroundColor: question.expanded
? theme.palette.background.default
: "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: !question.expanded ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
@ -174,7 +243,7 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
fontSize: "18px",
lineHeight: "21px",
py: 0,
paddingLeft: switchState.length === 0 ? 0 : "18px",
paddingLeft: question.type.length === 0 ? 0 : "18px",
},
}}
/>
@ -182,96 +251,132 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
<Box
sx={{
display: "flex",
alignItems: "flex-start",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
height: "30px",
marginBottom: "10px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
}}
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
onClick={() =>
updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, {
expanded: !question.expanded,
})
}
>
<IconButton
sx={{ paddingBottom: isMobile ? "0" : "" }}
disableRipple
onClick={() =>
updateQuestionsList(quizId, totalIndex, {
expanded: !isExpanded,
})
}
>
{isExpanded ? (
<ArrowDownIcon
style={{
width: "18px",
color: "#4D4D4D",
}}
/>
) : (
<ExpandLessIcon fill="#7E2AEA" />
)}
</IconButton>
{isExpanded ? (
<></>
{question.expanded ? (
<ArrowDownIcon
style={{
width: "18px",
color: "#4D4D4D",
}}
/>
) : (
<Box sx={{ display: "flex", borderRight: "solid 1px white" }}>
<FormControlLabel
control={
<Checkbox icon={<HideIcon style={{ color: "#7E2AEA" }} />} checkedIcon={<CrossedEyeIcon />} />
}
label={""}
sx={{
color: theme.palette.grey2.main,
ml: "-9px",
mr: 0,
userSelect: "none",
}}
/>
<IconButton onClick={() => copyQuestion(quizId, totalIndex)}>
<CopyIcon style={{ color: "white" }} />
</IconButton>
<IconButton
sx={{
cursor: "pointer",
borderRadius: "6px",
padding: "2px",
}}
onClick={() => {
const removedId = listQuestions[quizId][totalIndex].id;
if (listQuestions[quizId][totalIndex].deleteTimeoutId) {
clearTimeout(
listQuestions[quizId][totalIndex].deleteTimeoutId
);
}
removeQuestion(quizId, totalIndex);
const newTimeoutId = window.setTimeout(() => {
removeQuestionForce(quizId, removedId);
}, 5000);
updateQuestionsList(quizId, totalIndex, {
...listQuestions[quizId][totalIndex],
deleteTimeoutId: newTimeoutId,
});
}}
>
<DeleteIcon style={{ color: "white" }} />
</IconButton>
</Box>
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
{question.expanded ? (
<></>
) : (
<Box
sx={{
display: "flex",
height: "30px",
borderRight: "solid 1px #4D4D4D",
}}
>
<FormControlLabel
control={
<Checkbox
icon={
<HideIcon
style={{
boxSizing: "border-box",
color: "#7E2AEA",
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
padding: "3px",
}}
/>
}
checkedIcon={<CrossedEyeIcon />}
/>
}
label={""}
sx={{
color: theme.palette.grey2.main,
ml: "-9px",
mr: 0,
userSelect: "none",
}}
/>
<IconButton
sx={{ padding: "0" }}
onClick={() => copyQuestion(quizId, totalIndex)}
>
<CopyIcon
style={{ color: theme.palette.brightPurple.main }}
/>
</IconButton>
<IconButton
sx={{
cursor: "pointer",
borderRadius: "6px",
padding: "0",
margin: "0 5px 0 10px",
}}
onClick={() => {
const removedId = question.id;
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
<OneIcon style={{ fontSize: "30px", marginRight: "-2px" }} />
</Box>
removeQuestion(quizId, totalIndex);
const newTimeoutId = window.setTimeout(() => {
removeQuestionForce(quizId, removedId);
}, 5000);
updateQuestionsList<AnyQuizQuestion>(quizId, totalIndex, {
...question,
deleteTimeoutId: newTimeoutId,
});
}}
>
<DeleteIcon
style={{ color: theme.palette.brightPurple.main }}
/>
</IconButton>
</Box>
)}
<OneIcon
style={{
fontSize: "30px",
marginLeft: "3px",
color: !question.expanded ? "#FFF" : "",
fill: question.expanded
? "#EEE4FC"
: theme.palette.brightPurple.main,
}}
/>
<IconButton
disableRipple
sx={{
padding: isMobile ? "0" : "5px 10px 8px 8px",
position: isMobile ? "absolute" : "relative",
padding: isMobile ? "0" : "0 5px",
right: isMobile ? "0" : null,
bottom: isMobile ? "0" : null,
}}
@ -281,7 +386,7 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
</IconButton>
</Box>
</Box>
{isExpanded && (
{question.expanded && (
<Box
sx={{
display: "flex",
@ -290,7 +395,7 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
borderRadius: "12px",
}}
>
{switchState.length === 0 ? (
{question.type === "nonselected" ? (
<TypeQuestions totalIndex={totalIndex} />
) : (
<SwitchQuestionsPage totalIndex={totalIndex} />
@ -310,7 +415,7 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
}}
>
<Box
onClick={() => createQuestion(quizId, totalIndex + 1)}
onClick={() => createQuestion(quizId, "nonselected", totalIndex + 1)}
sx={{
display: plusVisible && !isDragging ? "flex" : "none",
width: "100%",
@ -326,7 +431,8 @@ export default function QuestionsPageCard({ totalIndex, draggableProps, isDraggi
backgroundPosition: "bottom",
backgroundRepeat: "repeat-x",
backgroundSize: "20px 1px",
backgroundImage: "radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
backgroundImage:
"radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
}}
/>
<PlusIcon />

@ -9,6 +9,8 @@ import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import SwitchDropDown from "./switchDropDown";
import ButtonsOptions from "../ButtonsOptions";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
interface Props {
totalIndex: number;
}
@ -19,28 +21,29 @@ export default function DropDown({ totalIndex }: Props) {
const { listQuestions } = questionStore();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const variants = listQuestions[quizId][totalIndex].content.variants;
const question = listQuestions[quizId][totalIndex] as QuizQuestionSelect;
const SSHC = (data: string) => {
setSwitchState(data);
};
const addNewAnswer = () => {
const answerNew = variants.slice();
answerNew.push({ answer: "", hints: "", emoji: "", image: "" });
const answerNew = question.content.variants.slice();
answerNew.push({ answer: "", hints: "", extendedText: "" });
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
return (
<>
<Box sx={{ pl: "20px", pr: "20px" }}>
{variants.length === 0 ? (
<Box
sx={{
padding: isMobile ? "15px 20px 20px 20px" : "20px 20px 20px 20px ",
}}
>
{question.content.variants.length === 0 ? (
<Typography
sx={{
padding: "0 0 33px 80px",
@ -53,13 +56,16 @@ export default function DropDown({ totalIndex }: Props) {
Добавьте ответ
</Typography>
) : (
<AnswerDraggableList variants={variants} totalIndex={totalIndex} />
<AnswerDraggableList
variants={question.content.variants}
totalIndex={totalIndex}
/>
)}
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: "17px",
marginBottom: "20px",
}}
>
<Link
@ -93,11 +99,7 @@ export default function DropDown({ totalIndex }: Props) {
)}
</Box>
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
<SwitchDropDown switchState={switchState} totalIndex={totalIndex} />
</>
);

@ -1,5 +1,11 @@
import { useParams } from "react-router-dom";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
Box,
Typography,
Tooltip,
useMediaQuery,
useTheme,
} from "@mui/material";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce";
@ -8,6 +14,8 @@ import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
type SettingDropDownProps = {
totalIndex: number;
};
@ -21,15 +29,16 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionSelect;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
const debounceAnswer = useDebouncedCallback((value) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.default = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
content: { ...question.content, default: value },
});
}, 1000);
return (
@ -38,6 +47,7 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
sx={{
position: "relative",
display: "flex",
gap: "20px",
width: "100%",
justifyContent: "space-between",
flexDirection: isMobile ? "column" : null,
@ -56,23 +66,30 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
}}
>
<Typography
sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки ответов
</Typography>
<CustomCheckbox
label={"Можно несколько"}
checked={listQuestions[quizId][totalIndex].content.multi}
checked={question.content.multi}
handleChange={({ target }) =>
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
multi: target.checked,
},
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
content: { ...question.content, multi: target.checked },
})
}
/>
<Box sx={{ display: isMobile ? "none" : "block", mt: isMobile ? "11px" : "6px" }}>
<Box
sx={{
display: isMobile ? "none" : "block",
mt: isMobile ? "11px" : "6px",
}}
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
@ -86,7 +103,7 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
</Typography>
<CustomTextField
placeholder={"Выберите вариант"}
text={listQuestions[quizId][totalIndex].content.default}
text={question.content.default}
onChange={({ target }) => debounceAnswer(target.value)}
/>
</Box>
@ -99,52 +116,56 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
pr: isFigmaTablte ? "19px" : "20px",
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
width: "100%",
gap: "14px",
width: isMobile ? "auto" : "100%",
}}
>
<Typography
sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки вопросов
</Typography>
<CustomCheckbox
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
checked={!question.required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
required: !e.target.checked,
});
}}
/>
<Box sx={{ position: "relative", display: "flex", alignItems: "center" }}>
<Box sx={{ position: "relative", display: "flex", alignItems: "flex-start" }}>
<CustomCheckbox
sx={{ height: isMobile ? "100%" : "26px", alignItems: isMobile ? "flex-start" : "center" }}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
<InfoIcon
sx={{
position: isMobile ? "absolute" : null,
display: "block",
right: isMobile ? "30px" : null,
}}
/>
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
<Box
sx={{
width: "85%",
width: "100%",
pt: "20px",
display: isMobile ? "block" : "none",
}}
@ -162,14 +183,14 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
</Typography>
<CustomTextField
placeholder={"Выберите вариант"}
text={listQuestions[quizId][totalIndex].content.default}
text={question.content.default}
onChange={({ target }) => debounceAnswer(target.value)}
/>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -14,149 +14,44 @@ import SwitchEmoji from "./switchEmoji";
import { AnswerDraggableList } from "../AnswerDraggableList";
import { EmojiPicker } from "@ui_kit/EmojiPicker";
import { EmojiIcons } from "@icons/EmojiIocns";
import AddEmoji from "@icons/questionsPage/addEmoji";
import PlusImage from "@icons/questionsPage/plus";
import { questionStore, updateQuestionsList } from "@root/questions";
import AddEmoji from "@icons/questionsPage/addEmoji";
import PlusImage from "@icons/questionsPage/plus";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
interface Props {
totalIndex: number;
}
export default function Emoji({ totalIndex }: Props) {
const [switchState, setSwitchState] = useState<string>("setting");
const [open, setOpen] = useState<boolean>(false);
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(
null
);
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const { listQuestions } = questionStore();
const quizId = Number(useParams().quizId);
const theme = useTheme();
const question = listQuestions[quizId][totalIndex] as QuizQuestionEmoji;
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const SSHC = (data: string) => {
setSwitchState(data);
};
return (
<>
{/* <Box sx={{ padding: "0 20px 0px 20px" }}>
<Box
sx={{
width: "100%",
border: "1px solid #9A9AAF",
borderRadius: "8px",
marginBottom: "15px",
}}
>
<TextField
fullWidth
focused={false}
placeholder={"Добавьте ответ"}
multiline={listQuestions[quizId][totalIndex].content.largeCheck}
InputProps={{
startAdornment: (
<>
<InputAdornment position="start">
<PointsIcon style={{ color: "#9A9AAF", fontSize: "30px" }} />
</InputAdornment>
{!isMobile && (
<Box
sx={{
width: "60px",
height: "40px",
background: "#EEE4FC",
display: "flex",
justifyContent: "space-between",
marginRight: "20px",
marginLeft: "12px",
}}
>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: "40px" }}>
<EmojiIcons fontSize="22px" color="#7E2AEA" />
</Box>
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#7E2AEA",
height: "100%",
width: "20px",
color: "white",
fontSize: "15px",
}}
>
+
</span>
</Box>
)}
</>
),
endAdornment: (
<InputAdornment position="end">
<IconButton sx={{ padding: "0" }} aria-describedby="my-popover-id">
<MessageIcon style={{ color: "#9A9AAF", fontSize: "30px", marginRight: "6.5px" }} />
</IconButton>
<Popover id="my-popover-id" anchorOrigin={{ vertical: "bottom", horizontal: "left" }} open={false}>
<TextareaAutosize style={{ margin: "10px" }} placeholder="Подсказка для этого ответа" />
</Popover>
<IconButton sx={{ padding: "0" }}>
<DeleteIcon style={{ color: theme.palette.grey2.main, marginRight: "-1px" }} />
</IconButton>
</InputAdornment>
),
}}
sx={{
"& .MuiInputBase-root": {
padding: "13.5px",
borderRadius: "10px",
background: "#ffffff",
height: "48px",
},
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
}}
inputProps={{
sx: { fontSize: "18px", lineHeight: "21px", py: 0 },
}}
/>
{isMobile && (
<Box sx={{ display: "flex", alignItems: "center", m: "8px", position: "relative" }}>
<Box sx={{ width: "100%", background: "#EEE4FC", height: "40px" }} />
<ImageAddIcons
style={{ position: "absolute", color: "#7E2AEA", fontSize: "20px", left: "45%", right: "55%" }}
/>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
}}
>
+
</Box>
</Box>
)}
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginBottom: "20px" }}> */}
<Box sx={{ padding: "20px" }}>
<AnswerDraggableList
variants={listQuestions[quizId][totalIndex].content.variants}
variants={question.content.variants}
totalIndex={totalIndex}
additionalContent={(variant, index) => (
<>
{!isTablet && (
<Box sx={{ cursor: "pointer", margin: "0 15px 0 5px" }}>
<Box sx={{ cursor: "pointer" }}>
<Box
onClick={({ currentTarget }) => {
setAnchorElement(currentTarget);
@ -172,19 +67,29 @@ export default function Emoji({ totalIndex }: Props) {
gap: "5px",
}}
>
{variant.emoji ? (
{variant.extendedText ? (
<Box
sx={{
width: "30px",
height: "40px",
width: "60px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "#EEE4FC",
borderRadius: "3px",
marginRight: "15px",
}}
>
<Box sx={{ marginLeft: "3px" }}>{variant.emoji}</Box>
<Box sx={{ marginLeft: "-3px" }}>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText}
</Box>
<Box>
<PlusImage />
</Box>
</Box>
@ -220,7 +125,7 @@ export default function Emoji({ totalIndex }: Props) {
height: "40px",
}}
/>
{variant.emoji ? (
{variant.extendedText ? (
<Box
sx={{
position: "absolute",
@ -230,7 +135,7 @@ export default function Emoji({ totalIndex }: Props) {
right: "55%",
}}
>
{variant.emoji}
{variant.extendedText}
</Box>
) : (
<EmojiIcons
@ -280,39 +185,30 @@ export default function Emoji({ totalIndex }: Props) {
<EmojiPicker
onEmojiSelect={({ native }) => {
setOpen(false);
const cloneVariants = [
...listQuestions[quizId][totalIndex].content.variants,
];
const cloneVariants = [...question.content.variants];
cloneVariants[currentIndex] = {
...cloneVariants[currentIndex],
emoji: native,
extendedText: native,
};
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: cloneVariants,
},
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
content: { ...question.content, variants: cloneVariants },
});
}}
/>
</Popover>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: isMobile ? "17px" : "20px" }}>
<Link
component="button"
variant="body2"
sx={{ color: theme.palette.brightPurple.main }}
onClick={() => {
const answerNew =
listQuestions[quizId][totalIndex].content.variants.slice();
answerNew.push({ answer: "", hints: "", emoji: "", image: "" });
const answerNew = question.content.variants.slice();
answerNew.push({ answer: "", hints: "", extendedText: "" });
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
}}
>
@ -341,11 +237,7 @@ export default function Emoji({ totalIndex }: Props) {
)}
</Box>
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
<SwitchEmoji switchState={switchState} totalIndex={totalIndex} />
</>
);

@ -6,6 +6,8 @@ import CustomTextField from "@ui_kit/CustomTextField";
import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
type SettingEmojiProps = {
totalIndex: number;
};
@ -19,10 +21,11 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionEmoji;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
return (
@ -51,21 +54,21 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={listQuestions[quizId][totalIndex].content.multi}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.multi = e.target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.multi}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
content: { ...question.content, multi: target.checked },
});
}}
/>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={listQuestions[quizId][totalIndex].content.own}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.own = e.target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.own}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
content: { ...question.content, own: target.checked },
});
}}
/>
</Box>
@ -77,8 +80,8 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
pr: isFigmaTablte ? "30px" : "20px",
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
width: "100%",
gap: "14px",
width: isMobile ? "auto" : "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
@ -87,27 +90,36 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
checked={!question.required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
required: !e.target.checked,
});
}}
/>
<Box sx={{ width: isMobile ? "90%" : "auto", display: "flex", alignItems: "center" }}>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
sx={{
mr: isMobile ? "0px" : "16px",
height: isMobile ? "100%" : "26px",
alignItems: isMobile ? "flex-start" : "center",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
@ -116,10 +128,10 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -0,0 +1,148 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import {
Box,
Typography,
Popper,
Grow,
Paper,
MenuList,
MenuItem,
ClickAwayListener,
Modal,
Button,
useTheme,
} from "@mui/material";
import {
questionStore,
updateQuestionsList,
removeQuestionForce,
createQuestion,
} from "@root/questions";
import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions";
import type { RefObject } from "react";
import type {
QuizQuestionType,
QuizQuestionBase,
} from "../../../../model/questionTypes/shared";
type ChooseAnswerModalProps = {
open: boolean;
onClose: () => void;
anchorRef: RefObject<HTMLDivElement>;
totalIndex: number;
switchState: string;
};
export const ChooseAnswerModal = ({
open,
onClose,
anchorRef,
totalIndex,
switchState,
}: ChooseAnswerModalProps) => {
const [openModal, setOpenModal] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<QuizQuestionType>("text");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme();
return (
<>
<Popper
placement="right-start"
open={open}
anchorEl={anchorRef.current}
transition
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={onClose}>
<MenuList autoFocusItem={open}>
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
<MenuItem
key={value}
sx={{ display: "flex", gap: "10px" }}
{...(value !== switchState && {
onClick: () => {
onClose();
setOpenModal(true);
setSelectedValue(value);
},
})}
>
<Box>{icon}</Box>
<Typography
sx={{
color:
value === switchState
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
}}
>
{title}
</Typography>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
<Modal open={openModal} onClose={() => setOpenModal(false)}>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
padding: "30px",
borderRadius: "10px",
background: "#FFFFFF",
}}
>
<Typography variant="h6">
Все настройки, кроме заголовка вопроса будут сброшены
</Typography>
<Box
sx={{
marginTop: "30px",
display: "flex",
justifyContent: "center",
gap: "15px",
}}
>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => setOpenModal(false)}
>
Отмена
</Button>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
setOpenModal(false);
const question = { ...listQuestions[quizId][totalIndex] };
removeQuestionForce(quizId, question.id);
createQuestion(quizId, selectedValue, totalIndex);
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
expanded: question.expanded,
});
}}
>
Подтвердить
</Button>
</Box>
</Box>
</Modal>
</>
);
};

@ -0,0 +1,92 @@
import { memo } from "react";
import { useParams } from "react-router-dom";
import { Draggable } from "react-beautiful-dnd";
import { Box, ListItem, Typography, useTheme } from "@mui/material";
import QuestionsPageCard from "./QuestionPageCard";
import { updateQuestionsList } from "@root/questions";
import { QuizQuestionBase } from "../../../../model/questionTypes/shared";
type FormDraggableListItemProps = {
index: number;
isDragging: boolean;
questionData: QuizQuestionBase;
};
export default memo(
({ index, isDragging, questionData }: FormDraggableListItemProps) => {
const quizId = Number(useParams().quizId);
const theme = useTheme();
console.log("Мой индекс " + index);
console.log(questionData);
return (
<Draggable draggableId={String(index)} index={index}>
{(provided) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
sx={{ userSelect: "none", padding: 0 }}
>
{questionData.deleted ? (
<Box
{...provided.dragHandleProps}
sx={{
width: "100%",
maxWidth: "800px",
display: "flex",
justifyContent: "center",
marginTop: "30px",
gap: "5px",
}}
>
<Typography
sx={{
fontSize: "16px",
color: theme.palette.grey2.main,
}}
>
Вопрос удалён.
</Typography>
<Typography
onClick={() => {
updateQuestionsList<QuizQuestionBase>(quizId, index, {
...questionData,
deleted: false,
});
}}
sx={{
cursor: "pointer",
fontSize: "16px",
textDecoration: "underline",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
>
Восстановить?
</Typography>
</Box>
) : (
<Box
sx={{
width: "100%",
position: "relative",
borderBottom: "1px solid rgba(0, 0, 0, 0.23)",
}}
>
<QuestionsPageCard
key={index}
totalIndex={index}
draggableProps={provided.dragHandleProps}
isDragging={isDragging}
/>
</Box>
)}
</ListItem>
)}
</Draggable>
);
}
);

@ -0,0 +1,168 @@
import { useState, useRef, useEffect } from "react";
import { useParams } from "react-router-dom";
import { Box, InputAdornment, Paper } from "@mui/material";
import { useDebouncedCallback } from "use-debounce";
import CustomTextField from "@ui_kit/CustomTextField";
import { ChooseAnswerModal } from "./ChooseAnswerModal";
import FormTypeQuestions from "../FormTypeQuestions";
import SwitchQuestionsPage from "../../SwitchQuestionsPage";
import { questionStore, updateQuestionsList } from "@root/questions";
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import Answer from "@icons/questionsPage/answer";
import OptionsPict from "@icons/questionsPage/options_pict";
import OptionsAndPict from "@icons/questionsPage/options_and_pict";
import Emoji from "@icons/questionsPage/emoji";
import Input from "@icons/questionsPage/input";
import DropDown from "@icons/questionsPage/drop_down";
import Date from "@icons/questionsPage/date";
import Slider from "@icons/questionsPage/slider";
import Download from "@icons/questionsPage/download";
import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating";
import AnswerGroup from "@icons/questionsPage/answerGroup";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import type { QuizQuestionBase } from "../../../../model/questionTypes/shared";
interface Props {
totalIndex: number;
draggableProps: DraggableProvidedDragHandleProps | null | undefined;
isDragging: boolean;
}
const IconAndrom = (switchState: string) => {
switch (switchState) {
case "variant":
return <Answer color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "images":
return (
<OptionsPict color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
case "varimg":
return (
<OptionsAndPict
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "emoji":
return <Emoji color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "text":
return <Input color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "select":
return (
<DropDown color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
case "date":
return <Date color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "number":
return <Slider color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "file":
return (
<Download color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
case "page":
return <Page color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "rating":
return (
<RatingIcon color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
default:
return (
<AnswerGroup color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
}
};
export default function QuestionsPageCard({
totalIndex,
draggableProps,
isDragging,
}: Props) {
const [open, setOpen] = useState<boolean>(false);
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex];
const anchorRef = useRef(null);
const debounced = useDebouncedCallback((title) => {
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, { title });
}, 1000);
useEffect(() => {
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
}, [question]);
return (
<>
<Paper
id={String(totalIndex)}
sx={{
overflow: "hidden",
maxWidth: "796px",
width: "100%",
backgroundColor: "white",
border: "none",
boxShadow: "none",
paddingBottom: "20px",
borderRadius: "0",
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: 0,
}}
>
<CustomTextField
placeholder={`Заголовок ${totalIndex + 1} вопроса`}
text={question.title}
onChange={({ target }) => debounced(target.value)}
sx={{ margin: "20px", width: "auto" }}
InputProps={{
startAdornment: (
<Box>
<InputAdornment
ref={anchorRef}
position="start"
sx={{ cursor: "pointer" }}
onClick={() => setOpen((isOpened) => !isOpened)}
>
{IconAndrom(question.type)}
</InputAdornment>
<ChooseAnswerModal
open={open}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
totalIndex={totalIndex}
switchState={question.type}
/>
</Box>
),
endAdornment: (
<>
<InputAdornment {...draggableProps} position="start">
<PointsIcon
style={{ color: "#9A9AAF", fontSize: "30px" }}
/>
</InputAdornment>
</>
),
}}
/>
{question.type === "nonselected" ? (
<FormTypeQuestions totalIndex={totalIndex} />
) : (
<SwitchQuestionsPage totalIndex={totalIndex} />
)}
</Box>
</Paper>
</>
);
}

@ -0,0 +1,11 @@
export const reorder = <T>(
list: T[],
startIndex: number,
endIndex: number
): T[] => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};

@ -0,0 +1,48 @@
import { useParams } from "react-router-dom";
import { Box } from "@mui/material";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import FormDraggableListItem from "./FormDraggableListItem";
import { questionStore, updateQuestionsListDragAndDrop } from "@root/questions";
import { reorder } from "./helper";
import type { DropResult } from "react-beautiful-dnd";
export const FormDraggableList = () => {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) {
const newItems = reorder(
listQuestions[quizId],
source.index,
destination.index
);
updateQuestionsListDragAndDrop(quizId, newItems);
}
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable-list">
{(provided, snapshot) => (
<Box ref={provided.innerRef} {...provided.droppableProps}>
{listQuestions[quizId]?.map((question, index) => (
<FormDraggableListItem
key={index}
index={index}
isDragging={snapshot.isDraggingOver}
questionData={question}
/>
))}
{provided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
);
};

@ -0,0 +1,142 @@
import { Box, Button, Typography, useTheme } from "@mui/material";
import { useParams } from "react-router-dom";
import { FormDraggableList } from "./FormDraggableList";
import {
questionStore,
createQuestion,
updateQuestionsList,
} from "@root/questions";
import { quizStore } from "@root/quizes";
import ArrowLeft from "../../../assets/icons/questionsPage/arrowLeft";
import AddAnswer from "../../../assets/icons/questionsPage/addAnswer";
import type {
AnyQuizQuestion,
QuizQuestionBase,
} from "../../../model/questionTypes/shared";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { createPortal } from "react-dom";
export default function FormQuestionsPage() {
const { listQuizes, updateQuizesList } = quizStore();
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const handleNext = () => {
updateQuizesList(quizId, { step: listQuizes[quizId].step + 1 });
};
const collapseEverything = () => {
listQuestions[quizId].forEach((item, index) => {
updateQuestionsList<AnyQuizQuestion>(quizId, index, {
...item,
expanded: false,
});
});
};
const theme = useTheme();
return (
<>
<Box
sx={{
maxWidth: "796px",
width: "100%",
display: "flex",
justifyContent: "space-between",
margin: "60px 0 40px 0",
}}
>
<Typography variant={"h5"}>Заголовок анкеты</Typography>
<Button
sx={{
fontSize: "16px",
lineHeight: "19px",
padding: 0,
textDecoration: "underline",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
onClick={collapseEverything}
>
Свернуть всё
</Button>
</Box>
<Box
sx={{
maxWidth: "796px",
boxShadow: "0px 10px 30px #e7e7e7",
borderRadius: "12px",
marginBottom: "40px",
borderTop: "1px solid transparent",
borderBottom: "1px solid transparent",
background: "#FFFFFF",
}}
>
<FormDraggableList />
<Box
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "15px",
padding: "4px",
margin: "15px",
border: "1px solid transparent",
borderRadius: "8px",
"&:hover": {
border: "1px solid #9A9AAF",
},
}}
onClick={() => {
createQuestion(quizId);
updateQuestionsList<QuizQuestionBase>(
quizId,
listQuestions[quizId].length - 1 || 0,
{
expanded: true,
}
);
}}
>
<AddAnswer color="#EEE4FC" />
<Typography sx={{ color: "#9A9AAF" }}>
Добавить еще один вопрос
</Typography>
</Box>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
gap: "8px",
maxWidth: "796px",
}}
>
<Button
variant="outlined"
sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }}
>
<ArrowLeft />
</Button>
<Button
variant="contained"
sx={{
height: "44px",
padding: "10px 20px",
borderRadius: "8px",
background: theme.palette.brightPurple.main,
fontSize: "18px",
}}
onClick={handleNext}
>
Следующий шаг
</Button>
{createPortal(<QuizPreview />, document.body)}
</Box>
</>
);
}

@ -0,0 +1,143 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import { Box } from "@mui/material";
import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton";
import ButtonsOptions from "../ButtonsOptions";
import SwitchAnswerOptions from "../answerOptions/switchAnswerOptions";
import Answer from "../../../assets/icons/questionsPage/answer";
import OptionsPict from "../../../assets/icons/questionsPage/options_pict";
import OptionsAndPict from "../../../assets/icons/questionsPage/options_and_pict";
import Emoji from "../../../assets/icons/questionsPage/emoji";
import Input from "../../../assets/icons/questionsPage/input";
import DropDown from "../../../assets/icons/questionsPage/drop_down";
import Date from "../../../assets/icons/questionsPage/date";
import Slider from "../../../assets/icons/questionsPage/slider";
import Download from "../../../assets/icons/questionsPage/download";
import Page from "../../../assets/icons/questionsPage/page";
import RatingIcon from "../../../assets/icons/questionsPage/rating";
import {
questionStore,
updateQuestionsList,
createQuestion,
removeQuestionForce,
} from "@root/questions";
import type {
QuizQuestionType,
QuizQuestionBase,
} from "../../../model/questionTypes/shared";
interface Props {
totalIndex: number;
}
type ButtonTypeQuestion = {
icon: JSX.Element;
title: string;
value: QuizQuestionType;
};
export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
{
icon: <Answer color="#9A9AAF" />,
title: "Варианты ответов",
value: "variant",
},
{
icon: <OptionsPict color="#9A9AAF" />,
title: "Варианты с картинками",
value: "images",
},
{
icon: <OptionsAndPict color="#9A9AAF" />,
title: "Варианты и картинка",
value: "varimg",
},
{
icon: <Emoji color="#9A9AAF" />,
title: "Эмоджи",
value: "emoji",
},
{
icon: <Input color="#9A9AAF" />,
title: "Своё поле для ввода",
value: "text",
},
{
icon: <DropDown color="#9A9AAF" />,
title: "Выпадающий список",
value: "select",
},
{
icon: <Date color="#9A9AAF" />,
title: "Дата",
value: "date",
},
{
icon: <Slider color="#9A9AAF" />,
title: "Ползунок",
value: "number",
},
{
icon: <Download color="#9A9AAF" />,
title: "Загрузка файла",
value: "file",
},
{
icon: <Page color="#9A9AAF" />,
title: "Страница",
value: "page",
},
{
icon: <RatingIcon color="#9A9AAF" />,
title: "Рейтинг",
value: "rating",
},
];
export default function FormTypeQuestions({ totalIndex }: Props) {
const [switchState, setSwitchState] = useState("");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
return (
<Box>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: "20px",
margin: "20px",
}}
>
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
<QuestionsMiniButton
key={title}
onClick={() => {
const clonedQuestion = { ...question };
removeQuestionForce(quizId, clonedQuestion.id);
createQuestion(quizId, value, totalIndex);
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
expanded: clonedQuestion.expanded,
type: value,
});
}}
icon={icon}
text={title}
/>
))}
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
totalIndex={totalIndex}
/>
<SwitchAnswerOptions switchState={switchState} totalIndex={totalIndex} />
</Box>
);
}

@ -1,16 +1,16 @@
import { useState } from "react";
import {
Box,
Link,
Typography,
useTheme,
useMediaQuery,
InputAdornment,
IconButton,
Button,
Popover,
TextareaAutosize,
TextField,
Box,
Link,
Typography,
useTheme,
useMediaQuery,
InputAdornment,
IconButton,
Button,
Popover,
TextareaAutosize,
TextField,
} from "@mui/material";
import { useParams } from "react-router-dom";
@ -31,403 +31,427 @@ import { MessageIcon } from "@icons/messagIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import PlusImage from "@icons/questionsPage/plus";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import { produce } from "immer";
interface Props {
totalIndex: number;
totalIndex: number;
}
export default function OptionsAndPicture({ totalIndex }: Props) {
const [open, setOpen] = useState(false);
const [opened, setOpened] = useState<boolean>(false);
const [switchState, setSwitchState] = useState("setting");
const [currentIndex, setCurrentIndex] = useState<number>(0);
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const [open, setOpen] = useState(false);
const [opened, setOpened] = useState<boolean>(false);
const [switchState, setSwitchState] = useState("setting");
const [currentIndex, setCurrentIndex] = useState<number>(0);
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionVarImg;
const SSHC = (data: string) => {
setSwitchState(data);
};
const SSHC = (data: string) => {
setSwitchState(data);
};
const uploadImage = (files: FileList | null) => {
if (files?.length) {
const [file] = Array.from(files);
const uploadImage = (files: FileList | null) => {
if (files?.length) {
const [file] = Array.from(files);
const clonContent = { ...listQuestions[quizId][totalIndex].content };
clonContent.variants[currentIndex].image = URL.createObjectURL(file);
updateQuestionsList(quizId, totalIndex, { content: clonContent });
const clonedContent = { ...question.content };
clonedContent.variants[currentIndex].extendedText =
URL.createObjectURL(file);
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: clonedContent,
});
setOpen(false);
setOpened(true);
}
};
setOpen(false);
setOpened(true);
}
};
const addNewAnswer = () => {
const answerNew =
listQuestions[quizId][totalIndex].content.variants.slice();
answerNew.push({ answer: "", hints: "", emoji: "", image: "" });
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
});
};
return (
<>
<Box sx={{ pl: "20px", pr: "20px" }}>
<AnswerDraggableList
variants={listQuestions[quizId][totalIndex].content.variants}
totalIndex={totalIndex}
additionalContent={(variant, index) => (
<>
{!isMobile && (
return (
<>
<Box sx={{ pl: "20px", pr: "20px" }}>
<AnswerDraggableList
variants={question.content.variants}
totalIndex={totalIndex}
additionalContent={(variant, index) => (
<>
{!isMobile && (
<Box
sx={{ cursor: "pointer" }}
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
>
{variant.extendedText ? (
<Box
sx={{
overflow: "hidden",
width: "60px",
display: "flex",
alignItems: "center",
background: "#EEE4FC",
borderRadius: "3px",
margin: "0 10px",
height: "40px",
}}
>
<Box sx={{ display: "flex", width: "40px" }}>
<img
src={variant.extendedText}
alt=""
style={{ width: "100%" }}
/>
</Box>
<PlusImage />
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<AddImage
sx={{
height: "40px",
width: "60px",
margin: "0 10px",
}}
/>
</Button>
)}
</Box>
)}
</>
)}
additionalMobile={(variant, index) => (
<>
{isMobile && (
<Box
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
sx={{
overflow: "hidden",
display: "flex",
alignItems: "center",
m: "8px",
position: "relative",
borderRadius: "3px",
}}
>
<Box
sx={{
width: "100%",
background: "#EEE4FC",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{variant.extendedText ? (
<Box
sx={{
overflow: "hidden",
width: "40px",
display: "flex",
alignItems: "center",
background: "#EEE4FC",
height: "30px",
borderRadius: "3px",
}}
>
<img
src={variant.extendedText}
alt=""
style={{ width: "100%" }}
/>
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<Image
sx={{
height: "40px",
width: "60px",
margin: "0 10px",
}}
/>
</Button>
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
}}
>
+
</Box>
</Box>
)}
</>
)}
/>
<UploadImageModal
open={open}
onClose={() => setOpen(false)}
imgHC={uploadImage}
/>
<CropModal
opened={opened}
onClose={() => setOpened(false)}
picture={question.content.variants[currentIndex]?.extendedText}
onCropPress={url => {
const content = produce(question.content, draft => {
draft.variants[currentIndex].extendedText = url;
});
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content,
});
}}
/>
<Box
sx={{ cursor: "pointer" }}
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
sx={{
width: "100%",
border: "1px solid #9A9AAF",
borderRadius: "8px",
display: isTablet ? "block" : "none",
}}
>
{variant.image ? (
<Box
sx={{
overflow: "hidden",
width: "60px",
<TextField
fullWidth
focused={false}
placeholder={"Добавьте ответ"}
multiline={question.content.largeCheck}
InputProps={{
startAdornment: (
<>
<InputAdornment position="start">
<PointsIcon
style={{ color: "#9A9AAF", fontSize: "30px" }}
/>
</InputAdornment>
{!isMobile && (
<Box
sx={{
width: "60px",
height: "40px",
background: "#EEE4FC",
display: "flex",
justifyContent: "space-between",
marginRight: "20px",
marginLeft: "12px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
<ImageAddIcons fontSize="22px" color="#7E2AEA" />
</Box>
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#7E2AEA",
height: "100%",
width: "25px",
color: "white",
fontSize: "15px",
}}
>
+
</span>
</Box>
)}
</>
),
endAdornment: (
<InputAdornment position="end">
<IconButton
sx={{ padding: "0" }}
aria-describedby="my-popover-id"
>
<MessageIcon
style={{
color: "#9A9AAF",
fontSize: "30px",
marginRight: "6.5px",
}}
/>
</IconButton>
<Popover
id="my-popover-id"
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
open={false}
>
<TextareaAutosize
style={{ margin: "10px" }}
placeholder="Подсказка для этого ответа"
/>
</Popover>
<IconButton sx={{ padding: "0" }}>
<DeleteIcon
style={{
color: theme.palette.grey2.main,
marginRight: "-1px",
}}
/>
</IconButton>
</InputAdornment>
),
}}
sx={{
"& .MuiInputBase-root": {
padding: "13.5px",
borderRadius: "10px",
background: "#ffffff",
height: "48px",
},
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
}}
inputProps={{
sx: { fontSize: "18px", lineHeight: "21px", py: 0 },
}}
/>
{isMobile && (
<Box
sx={{
display: "flex",
alignItems: "center",
m: "8px",
position: "relative",
}}
>
<Box
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
/>
<ImageAddIcons
style={{
position: "absolute",
color: "#7E2AEA",
fontSize: "20px",
left: "45%",
right: "55%",
}}
/>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
}}
>
<Box
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
/>
<ImageAddIcons
style={{
position: "absolute",
color: "#7E2AEA",
fontSize: "20px",
left: "45%",
right: "55%",
}}
/>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
}}
>
+
</Box>
</Box>
</Box>
)}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
background: "#EEE4FC",
borderRadius: "3px",
margin: "0 10px",
height: "40px",
}}
>
<Box sx={{ display: "flex", width: "40px" }}>
<img
src={variant.image}
alt=""
style={{ width: "100%" }}
/>
</Box>
<PlusImage />
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<AddImage
sx={{
height: "40px",
width: "60px",
margin: "0 10px",
}}
/>
</Button>
)}
</Box>
)}
</>
)}
additionalMobile={(variant, index) => (
<>
{isMobile && (
<Box
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
sx={{
overflow: "hidden",
display: "flex",
alignItems: "center",
m: "8px",
position: "relative",
borderRadius: "3px",
}}
marginBottom: "17px",
}}
>
<Box
sx={{
width: "100%",
background: "#EEE4FC",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{variant.image ? (
<Box
<Link
component="button"
variant="body2"
sx={{
overflow: "hidden",
width: "40px",
display: "flex",
alignItems: "center",
background: "#EEE4FC",
height: "30px",
borderRadius: "3px",
color: theme.palette.brightPurple.main,
fontWeight: "400",
fontSize: "16px",
mr: "4px",
height: "19px",
}}
onClick={() => {
const clonedContent = { ...question.content };
clonedContent.variants.push({
answer: "",
hints: "",
extendedText: "",
});
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: clonedContent,
});
}}
>
<img
src={variant.image}
alt=""
style={{ width: "100%" }}
/>
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<Image
sx={{
height: "40px",
width: "60px",
margin: "0 10px",
}}
/>
</Button>
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
}}
>
+
</Box>
</Box>
)}
</>
)}
/>
<UploadImageModal
open={open}
onClose={() => setOpen(false)}
imgHC={uploadImage}
/>
<CropModal
opened={opened}
onClose={() => setOpened(false)}
picture={
listQuestions[quizId][totalIndex].content.variants[currentIndex]
.image
}
/>
<Box
sx={{
width: "100%",
border: "1px solid #9A9AAF",
borderRadius: "8px",
display: isTablet ? "block" : "none",
}}
>
<TextField
fullWidth
focused={false}
placeholder={"Добавьте ответ"}
multiline={listQuestions[quizId][totalIndex].content.largeCheck}
InputProps={{
startAdornment: (
<>
<InputAdornment position="start">
<PointsIcon
style={{ color: "#9A9AAF", fontSize: "30px" }}
/>
</InputAdornment>
{!isMobile && (
<Box
sx={{
width: "60px",
height: "40px",
background: "#EEE4FC",
display: "flex",
justifyContent: "space-between",
marginRight: "20px",
marginLeft: "12px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
<ImageAddIcons fontSize="22px" color="#7E2AEA" />
</Box>
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#7E2AEA",
height: "100%",
width: "25px",
color: "white",
fontSize: "15px",
}}
>
+
</span>
</Box>
)}
</>
),
endAdornment: (
<InputAdornment position="end">
<IconButton
sx={{ padding: "0" }}
aria-describedby="my-popover-id"
>
<MessageIcon
style={{
color: "#9A9AAF",
fontSize: "30px",
marginRight: "6.5px",
}}
/>
</IconButton>
<Popover
id="my-popover-id"
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
open={false}
>
<TextareaAutosize
style={{ margin: "10px" }}
placeholder="Подсказка для этого ответа"
/>
</Popover>
<IconButton sx={{ padding: "0" }}>
<DeleteIcon
style={{
color: theme.palette.grey2.main,
marginRight: "-1px",
}}
/>
</IconButton>
</InputAdornment>
),
}}
sx={{
"& .MuiInputBase-root": {
padding: "13.5px",
borderRadius: "10px",
background: "#ffffff",
height: "48px",
},
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
}}
inputProps={{
sx: { fontSize: "18px", lineHeight: "21px", py: 0 },
}}
/>
{isMobile && (
<Box
sx={{
display: "flex",
alignItems: "center",
m: "8px",
position: "relative",
}}
>
<Box
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
/>
<ImageAddIcons
style={{
position: "absolute",
color: "#7E2AEA",
fontSize: "20px",
left: "45%",
right: "55%",
}}
/>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
}}
>
+
</Box>
Добавьте ответ
</Link>
{isMobile ? null : (
<>
<Typography
sx={{
fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
</Box>
)}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: "17px",
}}
>
<Link
component="button"
variant="body2"
sx={{
color: theme.palette.brightPurple.main,
fontWeight: "400",
fontSize: "16px",
mr: "4px",
height: "19px",
}}
onClick={() => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.variants.push({
answer: "",
hints: "",
emoji: "",
image: "",
});
updateQuestionsList(quizId, totalIndex, { content: clonContent });
}}
>
Добавьте ответ
</Link>
{isMobile ? null : (
<>
<Typography
sx={{
fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
</Box>
<ButtonsOptionsAndPict
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<SwitchOptionsAndPict switchState={switchState} totalIndex={totalIndex} />
</>
);
<ButtonsOptionsAndPict
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<SwitchOptionsAndPict switchState={switchState} totalIndex={totalIndex} />
</>
);
}

@ -7,6 +7,8 @@ import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
type SettingOptionsAndPictProps = {
totalIndex: number;
};
@ -19,19 +21,15 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(680));
const question = listQuestions[quizId][totalIndex] as QuizQuestionVarImg;
const debounced = useDebouncedCallback((replText) => {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
replText,
},
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: { ...question.content, replText },
});
}, 1000);
const debounceDescription = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
@ -46,7 +44,7 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
>
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pt: isMobile ? "30px" : "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
display: "flex",
@ -64,13 +62,10 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={listQuestions[quizId][totalIndex].content.own}
checked={question.content.own}
handleChange={({ target }) => {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
own: target.checked,
},
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: { ...question.content, own: target.checked },
});
}}
/>
@ -88,9 +83,13 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
Текст-заглушка на картинке
</Typography>
<CustomTextField
sx={{ maxWidth: "360px", width: "100%", mr: isMobile ? "0px" : "16px" }}
sx={{
maxWidth: "330px",
width: "100%",
mr: isMobile ? "0px" : "16px",
}}
placeholder={"Пример текста"}
text={listQuestions[quizId][totalIndex].content.replText}
text={question.content.replText}
onChange={({ target }) => debounced(target.value)}
/>
</Box>
@ -105,7 +104,7 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
width: "100%",
width: isMobile ? "auto" : "100%",
}}
>
<Typography
@ -116,25 +115,25 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={listQuestions[quizId][totalIndex].content.required}
checked={question.content.required}
handleChange={({ target }) => {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
required: target.checked,
},
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: { ...question.content, required: target.checked },
});
}}
/>
<Box sx={{ display: "flex", alignItems: "center" }}>
<CustomCheckbox
sx={{ width: isMobile ? "90%" : "auto", mr: isMobile ? "0px" : "16px" }}
sx={{
width: isMobile ? "90%" : "auto",
mr: isMobile ? "0px" : "16px",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
...question.content,
innerNameCheck: target.checked,
innerName: "",
},
@ -147,10 +146,10 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounceDescription(target.value)}
/>
)}
@ -164,7 +163,7 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
<CustomTextField
sx={{ maxWidth: "360px", width: "100%" }}
placeholder={"Пример текста"}
text={listQuestions[quizId][totalIndex].content.replText}
text={question.content.replText}
onChange={({ target }) => debounced(target.value)}
/>
</>

@ -1,12 +1,12 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import {
Box,
Link,
Typography,
Button,
useTheme,
useMediaQuery,
Box,
Link,
Typography,
Button,
useTheme,
useMediaQuery
} from "@mui/material";
import ButtonsOptions from "../ButtonsOptions";
@ -21,229 +21,231 @@ import Image from "../../../assets/icons/questionsPage/image";
import SwitchAnswerOptionsPict from "./switchOptionsPict";
import PlusImage from "@icons/questionsPage/plus";
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
import { produce } from "immer";
interface Props {
totalIndex: number;
totalIndex: number;
}
export default function OptionsPicture({ totalIndex }: Props) {
const [open, setOpen] = useState(false);
const [opened, setOpened] = useState<boolean>(false);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizId = Number(useParams().quizId);
const [switchState, setSwitchState] = useState("setting");
const { listQuestions } = questionStore();
const [open, setOpen] = useState(false);
const [opened, setOpened] = useState<boolean>(false);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(790));
const quizId = Number(useParams().quizId);
const [switchState, setSwitchState] = useState("setting");
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
const SSHC = (data: string) => {
setSwitchState(data);
};
const SSHC = (data: string) => {
setSwitchState(data);
};
const uploadImage = (files: FileList | null) => {
if (files?.length) {
const [file] = Array.from(files);
const uploadImage = (files: FileList | null) => {
if (files?.length) {
const [file] = Array.from(files);
const clonContent = { ...listQuestions[quizId][totalIndex].content };
clonContent.variants[currentIndex].image = URL.createObjectURL(file);
updateQuestionsList(quizId, totalIndex, { content: clonContent });
const clonedContent = { ...question.content };
clonedContent.variants[currentIndex].extendedText =
URL.createObjectURL(file);
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: clonedContent,
});
setOpen(false);
setOpened(true);
}
};
setOpen(false);
setOpened(true);
}
};
const addNewAnswer = () => {
const answerNew =
listQuestions[quizId][totalIndex].content.variants.slice();
answerNew.push({ answer: "", hints: "", emoji: "", image: "" });
const addNewAnswer = () => {
const answerNew = question.content.variants.slice();
answerNew.push({ answer: "", hints: "", extendedText: "" });
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
});
};
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
return (
<>
<Box sx={{ padding: "20px" }}>
<AnswerDraggableList
variants={listQuestions[quizId][totalIndex].content.variants}
totalIndex={totalIndex}
additionalContent={(variant, index) => (
<>
{!isMobile && (
<Box
sx={{ cursor: "pointer" }}
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
>
{variant.image ? (
<Box
sx={{
overflow: "hidden",
width: "60px",
display: "flex",
alignItems: "center",
background: "#EEE4FC",
borderRadius: "3px",
margin: "0 10px",
height: "40px",
}}
>
<Box sx={{ display: "flex", width: "40px" }}>
<img
src={variant.image}
alt=""
style={{ width: "100%" }}
/>
</Box>
<PlusImage />
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<AddImage
sx={{ height: "40px", width: "60px", margin: "0 10px" }}
/>
</Button>
)}
</Box>
)}
</>
)}
additionalMobile={(variant, index) => (
<>
{isMobile && (
<Box
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
sx={{
overflow: "hidden",
display: "flex",
alignItems: "center",
m: "8px",
position: "relative",
borderRadius: "3px",
}}
>
<Box
sx={{
width: "100%",
background: "#EEE4FC",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{variant.image ? (
<Box
sx={{
overflow: "hidden",
width: "40px",
display: "flex",
alignItems: "center",
background: "#EEE4FC",
height: "30px",
borderRadius: "3px",
}}
>
<img
src={variant.image}
alt=""
style={{ width: "100%" }}
/>
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<Image
sx={{
height: "40px",
width: "60px",
margin: "0 10px",
}}
/>
</Button>
return (
<>
<Box sx={{ padding: "20px" }}>
<AnswerDraggableList
variants={question.content.variants}
totalIndex={totalIndex}
additionalContent={(variant, index) => (
<>
{!isMobile && (
<Box
sx={{ cursor: "pointer" }}
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
>
{variant.extendedText ? (
<Box
sx={{
overflow: "hidden",
width: "60px",
display: "flex",
alignItems: "center",
background: "#EEE4FC",
borderRadius: "3px",
margin: "0 10px",
height: "40px",
}}
>
<Box sx={{ display: "flex", width: "40px" }}>
<img
src={variant.extendedText}
alt=""
style={{ width: "100%" }}
/>
</Box>
<PlusImage />
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<AddImage
sx={{ height: "40px", width: "60px", margin: "0 10px" }}
/>
</Button>
)}
</Box>
)}
</>
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
additionalMobile={(variant, index) => (
<>
{isMobile && (
<Box
onClick={() => {
setCurrentIndex(index);
setOpen(true);
}}
sx={{
overflow: "hidden",
display: "flex",
alignItems: "center",
m: "8px",
position: "relative",
borderRadius: "3px",
}}
>
<Box
sx={{
width: "100%",
background: "#EEE4FC",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{variant.extendedText ? (
<Box
sx={{
overflow: "hidden",
width: "40px",
display: "flex",
alignItems: "center",
background: "#EEE4FC",
height: "30px",
borderRadius: "3px",
}}
>
<img
src={variant.extendedText}
alt=""
style={{ width: "100%" }}
/>
</Box>
) : (
<Button component="label" sx={{ padding: "0px" }}>
<Image
sx={{
height: "40px",
width: "60px",
margin: "0 10px",
}}
/>
</Button>
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
}}
>
+
</Box>
</Box>
)}
</>
)}
/>
<UploadImageModal
open={open}
onClose={() => setOpen(false)}
imgHC={uploadImage}
/>
<CropModal
opened={opened}
onClose={() => setOpened(false)}
picture={question.content.variants[currentIndex]?.extendedText}
onCropPress={url => {
const content = produce(question.content, draft => {
draft.variants[currentIndex].extendedText = url;
});
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content,
});
}}
>
+
</Box>
/>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Link
component="button"
variant="body2"
sx={{ color: theme.palette.brightPurple.main }}
onClick={addNewAnswer}
>
Добавьте ответ
</Link>
{isMobile ? null : (
<>
<Typography
sx={{
fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
)}
</>
)}
/>
<UploadImageModal
open={open}
onClose={() => setOpen(false)}
imgHC={uploadImage}
/>
<CropModal
opened={opened}
onClose={() => setOpened(false)}
picture={
listQuestions[quizId][totalIndex].content.variants[currentIndex]
.image
}
/>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Link
component="button"
variant="body2"
sx={{ color: theme.palette.brightPurple.main }}
onClick={addNewAnswer}
>
Добавьте ответ
</Link>
{isMobile ? null : (
<>
<Typography
sx={{
fontWeight: 400,
lineHeight: "21.33px",
color: theme.palette.grey2.main,
fontSize: "16px",
}}
>
или нажмите Enter
</Typography>
<EnterIcon
style={{
color: "#7E2AEA",
fontSize: "24px",
marginLeft: "6px",
}}
/>
</>
)}
</Box>
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<SwitchAnswerOptionsPict
switchState={switchState}
totalIndex={totalIndex}
/>
</>
);
</Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
<SwitchAnswerOptionsPict switchState={switchState} totalIndex={totalIndex} />
</>
);
}

@ -1,6 +1,13 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { Box, Button, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material";
import {
Box,
Button,
Typography,
Tooltip,
useMediaQuery,
useTheme,
} from "@mui/material";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce";
@ -14,8 +21,11 @@ import ProportionsIcon11 from "../../../assets/icons/questionsPage/ProportionsIc
import ProportionsIcon21 from "../../../assets/icons/questionsPage/ProportionsIcon21";
import ProportionsIcon12 from "../../../assets/icons/questionsPage/ProportionsIcon12";
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
interface Props {
Icon: React.ElementType;
Icon: (props: { color: string }) => JSX.Element;
// Icon: React.ElementType;
isActive?: boolean;
onClick: () => void;
}
@ -24,7 +34,14 @@ type SettingOpytionsPictProps = {
totalIndex: number;
};
const PROPORTIONS = [
type Proportion = "1:1" | "2:1" | "1:2";
type ProportionItem = {
value: Proportion;
icon: (props: { color: string }) => JSX.Element;
};
const PROPORTIONS: ProportionItem[] = [
{ value: "1:1", icon: ProportionsIcon11 },
{ value: "2:1", icon: ProportionsIcon21 },
{ value: "1:2", icon: ProportionsIcon12 },
@ -37,13 +54,23 @@ export function SelectIconButton({ Icon, isActive = false, onClick }: Props) {
<Button
onClick={onClick}
variant="outlined"
startIcon={<Icon color={isActive ? theme.palette.navbarbg.main : theme.palette.brightPurple.main} />}
startIcon={
<Icon
color={
isActive
? theme.palette.navbarbg.main
: theme.palette.brightPurple.main
}
/>
}
sx={{
backgroundColor: isActive ? theme.palette.brightPurple.main : "#eee4fc",
borderRadius: 0,
border: "none",
color: isActive ? theme.palette.brightPurple.main : theme.palette.grey2.main,
color: isActive
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
p: "7px",
width: "40px",
height: "40px",
@ -54,39 +81,42 @@ export function SelectIconButton({ Icon, isActive = false, onClick }: Props) {
},
"&:hover": {
border: "none",
borderColor: isActive ? theme.palette.brightPurple.main : theme.palette.grey2.main,
borderColor: isActive
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
},
}}
/>
);
}
export default function SettingOpytionsPict({ totalIndex }: SettingOpytionsPictProps) {
export default function SettingOpytionsPict({
totalIndex,
}: SettingOpytionsPictProps) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(985));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
useEffect(() => {
if (!listQuestions[quizId][totalIndex].content.xy) {
if (!question.content.xy) {
updateProportions("1:1");
}
}, []);
const updateProportions = (proportions: string) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.xy = proportions;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
const updateProportions = (proportions: Proportion) => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, xy: proportions },
});
};
return (
@ -112,7 +142,14 @@ export default function SettingOpytionsPict({ totalIndex }: SettingOpytionsPictP
}}
>
<Box sx={{ pb: isMobile ? "11px" : "6px" }}>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D", mb: isMobile ? "10px" : "14px" }}>
<Typography
sx={{
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
mb: isMobile ? "10px" : "14px",
}}
>
Пропорции
</Typography>
<Box
@ -125,38 +162,39 @@ export default function SettingOpytionsPict({ totalIndex }: SettingOpytionsPictP
<SelectIconButton
key={index}
onClick={() => updateProportions(value)}
isActive={listQuestions[quizId][totalIndex].content.xy === value}
isActive={question.content.xy === value}
Icon={icon}
/>
))}
</Box>
</Box>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>Настройки ответов</Typography>
<Typography
sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}
>
Настройки ответов
</Typography>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={listQuestions[quizId][totalIndex].content.multi}
checked={question.content.multi}
handleChange={({ target }) =>
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
multi: target.checked,
},
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, multi: target.checked },
})
}
/>
{listQuestions[quizId][totalIndex].content.xy !== "1:1" &&
listQuestions[quizId][totalIndex].content.format !== "masonry" && (
{question.content.xy !== "1:1" &&
question.content.format !== "masonry" && (
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Большие картинки"}
checked={listQuestions[quizId][totalIndex].content.largeCheck}
checked={question.content.largeCheck}
handleChange={({ target }) =>
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
...question.content,
largeCheck: target.checked,
},
})
@ -166,75 +204,78 @@ export default function SettingOpytionsPict({ totalIndex }: SettingOpytionsPictP
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={listQuestions[quizId][totalIndex].content.own}
checked={question.content.own}
handleChange={({ target }) =>
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
own: target.checked,
},
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, own: target.checked },
})
}
/>
</Box>
<Box
sx={{
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isTablet ? "20px" : "",
pr: isFigmaTablte ? "30px" : "20px",
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
pr: isFigmaTablte ? (isMobile ? "20px" : "0px") : "28px",
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
gap: "14px",
width: "100%",
}}
>
<Box sx={{ pb: isMobile ? "11px" : "6px" }}>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D", mb: isMobile ? "10px" : "14px" }}>
Формат
</Typography>
<Box
<Box
sx={{
marginBottom: "5px",
opacity: question.content.xy !== "1:1" ? 1 : 0,
display: isTablet
? question.content.xy === "1:1"
? "none"
: "block"
: "block",
}}
>
<Typography
sx={{
display: "flex",
gap: "10px",
marginBottom: "15px",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
<SelectIconButton
onClick={() =>
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
format: "carousel",
},
})
}
isActive={listQuestions[quizId][totalIndex].content.format === "carousel"}
Icon={FormatIcon2}
/>
<SelectIconButton
onClick={() =>
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
format: "masonry",
},
})
}
isActive={listQuestions[quizId][totalIndex].content.format === "masonry"}
Icon={FormatIcon1}
/>
</Box>
Формат
</Typography>
<SelectIconButton
onClick={() =>
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, format: "carousel" },
})
}
isActive={question.content.format === "carousel"}
Icon={FormatIcon2}
/>
<SelectIconButton
onClick={() =>
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, format: "masonry" },
})
}
isActive={question.content.format === "masonry"}
Icon={FormatIcon1}
/>
</Box>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>Настройки вопросов</Typography>
<Typography
sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}
>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ alignItems: isMobile ? "flex-start" : "" }}
label={"Необязательный вопрос"}
checked={listQuestions[quizId][totalIndex].content.required}
checked={question.content.required}
handleChange={({ target }) =>
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
required: target.checked,
},
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, required: target.checked },
})
}
/>
@ -246,28 +287,35 @@ export default function SettingOpytionsPict({ totalIndex }: SettingOpytionsPictP
}}
>
<CustomCheckbox
sx={{
height: isMobile ? "100%" : "26px",
alignItems: isMobile ? "flex-start" : "center",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
checked={question.content.innerNameCheck}
handleChange={({ target }) =>
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
...question.content,
innerNameCheck: target.checked,
innerName: "",
},
})
}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Внутреннее описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -11,6 +11,8 @@ import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
interface Props {
totalIndex: number;
}
@ -19,13 +21,13 @@ export default function OwnTextField({ totalIndex }: Props) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme();
const isWrapperColumn = useMediaQuery(theme.breakpoints.down(980));
const question = listQuestions[quizId][totalIndex] as QuizQuestionText;
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.placeholder = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, {
content: { ...question.content, placeholder: value },
});
}, 1000);
const SSHC = (data: string) => {
@ -39,7 +41,7 @@ export default function OwnTextField({ totalIndex }: Props) {
width: "auto",
maxWidth: "745px",
display: "flex",
pb: "15px",
pb: "20px",
pl: "20px",
pr: "20px",
flexDirection: "column",
@ -48,11 +50,11 @@ export default function OwnTextField({ totalIndex }: Props) {
>
<CustomTextField
placeholder={"Пример ответа"}
text={listQuestions[quizId][totalIndex].content.placeholder}
text={question.content.placeholder}
onChange={({ target }) => debounced(target.value)}
sx={{ maxWidth: isFigmaTablte ? "549px" : "640px", width: "100%" }}
sx={{ maxWidth: isFigmaTablte ? "549px" : "640px", width: "100%", mt: isMobile ? "15px" : "0px" }}
/>
<Box sx={{ display: "flex", alignItems: "center", gap: "12px" }}>
<Box sx={{ display: "flex", alignItems: isMobile ? "flex-start" : "center", gap: "12px" }}>
<Typography
sx={{
fontWeight: 400,

@ -20,6 +20,8 @@ import CheckedIcon from "@ui_kit/RadioCheck";
import CheckIcon from "@ui_kit/RadioIcon";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
type SettingTextFieldProps = {
totalIndex: number;
};
@ -41,12 +43,13 @@ export default function SettingTextField({ totalIndex }: SettingTextFieldProps)
const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionText;
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
return (
@ -73,17 +76,16 @@ export default function SettingTextField({ totalIndex }: SettingTextFieldProps)
sx={{ display: "flex", flexDirection: "column", gap: "14px", width: "100%" }}
aria-labelledby="demo-controlled-radio-buttons-group"
name="controlled-radio-buttons-group"
value={ANSWER_TYPES.findIndex(({ value }) => listQuestions[quizId][totalIndex].content[value])}
value={ANSWER_TYPES.findIndex(
({ value }) => question.content.answerType === value
)}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
const clonContent = {
...listQuestions[quizId][totalIndex].content,
single: false,
multi: false,
number: false,
[ANSWER_TYPES[Number(target.value)].value]: true,
};
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, {
content: {
...question.content,
answerType: ANSWER_TYPES[Number(target.value)].value,
},
});
}}
>
{ANSWER_TYPES.map(({ name }, index) => (
@ -107,49 +109,67 @@ export default function SettingTextField({ totalIndex }: SettingTextFieldProps)
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isFigmaTablte ? (isWrappColumn ? "20px" : "34px") : "20px",
pr: isFigmaTablte ? "14px" : "20px",
pr: "20px",
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
gap: "14px",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
alignItems: isMobile ? "flex-end" : "center",
}}
label={"Автозаполнение адреса"}
checked={listQuestions[quizId][totalIndex].content.autofill}
checked={question.content.autofill}
handleChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.autofill = target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, {
content: { ...question.content, autofill: target.checked },
});
}}
/>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
alignItems: isMobile ? "flex-end" : "center",
}}
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
checked={!question.required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, {
required: !e.target.checked,
});
}}
/>
<Box sx={{ width: isMobile ? "90%" : "auto", display: "flex", alignItems: "center" }}>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
sx={{
alignItems: isMobile ? "flex-start" : "center",
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
height: isMobile ? "100%" : "26px",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
@ -158,10 +178,10 @@ export default function SettingTextField({ totalIndex }: SettingTextFieldProps)
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -15,6 +15,8 @@ import { AddPlusVideo } from "@icons/questionsPage/addPlusVideo";
import { ImageAddIcons } from "@icons/ImageAddIcons";
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
type Props = {
disableInput?: boolean;
totalIndex: number;
@ -30,10 +32,11 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
const isTablet = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(780));
const question = listQuestions[quizId][totalIndex] as QuizQuestionPage;
const debounced = useDebouncedCallback((value) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.text = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
content: { ...question.content, text: value },
});
}, 1000);
const SSHC = (data: string) => {
@ -52,13 +55,14 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
gap: isMobile ? "25px" : "20px",
}}
>
<Box sx={{ display: disableInput ? "none" : "" }}>
<Box sx={{ display: disableInput ? "none" : "", mt: isMobile ? "15px" : "0px" }}>
<CustomTextField
placeholder={"Можно добавить текст"}
text={listQuestions[quizId][totalIndex].content.text}
text={question.content.text}
onChange={({ target }) => debounced(target.value)}
/>
</Box>
<Box
sx={{
mb: "20px",
@ -79,7 +83,50 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
}}
>
{isMobile ? (
<AddPlusImage />
<Box
sx={{
display: "flex",
alignItems: "center",
width: "120px",
position: "relative",
}}
>
<Box
sx={{
width: "100%",
background: "#EEE4FC",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderTopLeftRadius: "4px",
borderBottomLeftRadius: "4px",
}}
>
<ImageAddIcons
style={{
color: "#7E2AEA",
fontSize: "20px",
}}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
borderTopRightRadius: "4px",
borderBottomRightRadius: "4px",
}}
>
+
</Box>
</Box>
) : (
<Box
sx={{
@ -127,10 +174,11 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
onClose={() => setOpenImageModal(false)}
imgHC={(fileList) => {
if (fileList?.length) {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.picture = URL.createObjectURL(fileList[0]);
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
content: {
...question.content,
picture: URL.createObjectURL(fileList[0]),
},
});
}
}}
@ -146,7 +194,50 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
}}
>
{isMobile ? (
<AddPlusVideo />
<Box
sx={{
display: "flex",
alignItems: "center",
width: "120px",
position: "relative",
}}
>
<Box
sx={{
width: "100%",
background: "#EEE4FC",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderTopLeftRadius: "4px",
borderBottomLeftRadius: "4px",
}}
>
<VideofileIcon
style={{
color: "#7E2AEA",
fontSize: "20px",
}}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
borderTopRightRadius: "4px",
borderBottomRightRadius: "4px",
}}
>
+
</Box>
</Box>
) : (
<Box
sx={{
@ -192,11 +283,11 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
<UploadVideoModal
open={openVideoModal}
onClose={() => setOpenVideoModal(false)}
video={listQuestions[quizId][totalIndex].content.video}
video={question.content.video}
onUpload={(url) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.video = url;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
content: { ...question.content, video: url },
});
}}
/>
</Box>

@ -14,63 +14,81 @@ import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
type SettingPageOptionsProps = {
totalIndex: number;
};
export default function SettingPageOptions({ totalIndex }: SettingPageOptionsProps) {
export default function SettingPageOptions({
totalIndex,
}: SettingPageOptionsProps) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionPage;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
return (
<Box
sx={{
boxSizing: "border-box",
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
pr: "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
width: isMobile ? "auto" : "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки вопроса
</Typography>
<Box sx={{ width: isMobile ? "90%" : "auto", display: "flex", alignItems: "center" }}>
<Box sx={{ display: "flex", alignItems: "flex-start" }}>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
sx={{
mr: isMobile ? "0px" : "16px",
height: isMobile ? "100%" : "26px",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
checked={question.content.innerNameCheck}
handleChange={({ target }) =>
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
...question.content,
innerNameCheck: target.checked,
innerName: "",
},
})
}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Внутреннее описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -1,43 +1,50 @@
import {
Box,
Button,
IconButton,
Typography,
useMediaQuery,
useTheme,
Box,
Button,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import AddPlus from "../../assets/icons/questionsPage/addPlus";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import { quizStore } from "@root/quizes";
import { useParams } from "react-router-dom";
import {
questionStore,
createQuestion,
updateQuestionsList,
questionStore,
createQuestion,
updateQuestionsList,
} from "@root/questions";
import { DraggableList } from "./DraggableList";
export default function QuestionsPage() {
const { listQuizes, updateQuizesList } = quizStore();
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const handleNext = () => {
updateQuizesList(quizId, { step: listQuizes[quizId].step + 1 });
};
import type { AnyQuizQuestion } from "../../model/questionTypes/shared";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { createPortal } from "react-dom";
const handleBack = () => {
let result = listQuizes[quizId].step - 1;
updateQuizesList(quizId, { step: result ? result : 1 });
};
export default function QuestionsPage() {
const { listQuizes, updateQuizesList } = quizStore();
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const handleNext = () => {
updateQuizesList(quizId, { step: listQuizes[quizId].step + 1 });
};
const handleBack = () => {
let result = listQuizes[quizId].step - 1;
updateQuizesList(quizId, { step: result ? result : 1 });
};
const collapseEverything = () => {
listQuestions[quizId].forEach((item, index) => {
updateQuestionsList(quizId, index, { ...item, expanded: false });
updateQuestionsList<AnyQuizQuestion>(quizId, index, {
...item,
expanded: false,
});
});
};
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.up(1000));
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.up(1000));
return (
<>
@ -103,6 +110,7 @@ export default function QuestionsPage() {
</Button>
</Box>
</Box>
{createPortal(<QuizPreview />, document.body)}
</>
);
}

@ -20,12 +20,14 @@ import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon";
import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon";
import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
interface Props {
totalIndex: number;
}
export type ButtonRatingFrom = {
name: string;
name: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag";
icon: JSX.Element;
};
@ -39,32 +41,29 @@ export default function RatingOptions({ totalIndex }: Props) {
const { listQuestions } = questionStore();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionRating;
const negativeRef = useRef<HTMLDivElement>(null);
const positiveRef = useRef<HTMLDivElement>(null);
const debounceNegativeDescription = useDebouncedCallback((value) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionRating>(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
...question.content,
ratingNegativeDescription: value.substring(0, 15),
},
});
}, 500);
const debouncePositiveDescription = useDebouncedCallback((value) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionRating>(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
...question.content,
ratingPositiveDescription: value.substring(0, 15),
},
});
}, 500);
useEffect(() => {
setNegativeText(
listQuestions[quizId][totalIndex].content.ratingNegativeDescription
);
setPositiveText(
listQuestions[quizId][totalIndex].content.ratingPositiveDescription
);
setNegativeText(question.content.ratingNegativeDescription);
setPositiveText(question.content.ratingPositiveDescription);
}, []);
useEffect(() => {
@ -76,7 +75,10 @@ export default function RatingOptions({ totalIndex }: Props) {
}, [positiveText]);
const buttonRatingForm: ButtonRatingFrom[] = [
{ name: "star", icon: <StarIconMini width={"50px"} color={theme.palette.grey2.main} /> },
{
name: "star",
icon: <StarIconMini width={"50px"} color={theme.palette.grey2.main} />,
},
{ name: "trophie", icon: <TropfyIcon color={theme.palette.grey2.main} /> },
{ name: "flag", icon: <FlagIcon color={theme.palette.grey2.main} /> },
{ name: "heart", icon: <HeartIcon color={theme.palette.grey2.main} /> },
@ -96,24 +98,17 @@ export default function RatingOptions({ totalIndex }: Props) {
<>
<Box
sx={{
width: isMobile
? "auto"
: `${
listQuestions[quizId][totalIndex].content.steps * 44 >
negativeTextWidth + positiveTextWidth
? listQuestions[quizId][totalIndex].content.steps * 44
: negativeTextWidth + positiveTextWidth + 20
}px`,
minWidth: "300px",
maxWidth: "440px",
display: "flex",
px: "20px",
flexDirection: "column",
gap: "20px",
marginTop: isMobile ? "20px" : 0,
}}
>
<Box
sx={{
width: isMobile ? "auto" : `${question.content.steps * 44}px`,
maxWidth: "440px",
display: "flex",
justifyContent: "space-between",
padding: "0 10px",
@ -121,14 +116,39 @@ export default function RatingOptions({ totalIndex }: Props) {
}}
>
{Array.from(
{ length: listQuestions[quizId][totalIndex].content.steps },
{ length: question.content.steps },
(_, index) => index
).map((itemNumber) => (
<Box key={itemNumber} sx={{ transform: "scale(1.5)" }}>
<Box
key={itemNumber}
{...(itemNumber === 0 || itemNumber === question.content.steps - 1
? {
onClick: () => {
updateQuestionsList<QuizQuestionRating>(
quizId,
totalIndex,
{
content: {
...question.content,
ratingExpanded: true,
},
}
);
},
sx: {
cursor: "pointer",
transform: "scale(1.5)",
":hover": {
transform: "scale(1.7)",
transition: "0.2s",
},
},
}
: { sx: { transform: "scale(1.5)" } })}
>
{
buttonRatingForm.find(
({ name }) =>
listQuestions[quizId][totalIndex].content.form === name
({ name }) => question.content.form === name
)?.icon
}
</Box>
@ -140,113 +160,118 @@ export default function RatingOptions({ totalIndex }: Props) {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
maxWidth: isMobile ? "303px" : "290px",
maxWidth: "410px",
width: "100%",
}}
>
<Typography
ref={negativeRef}
sx={{
position: "absolute",
opacity: 0,
zIndex: "-100",
whiteSpace: "nowrap",
fontSize: "16px",
}}
>
{negativeText}
</Typography>
<TextField
defaultValue={
listQuestions[quizId][totalIndex].content
.ratingNegativeDescription
}
value={negativeText}
placeholder="Негативно"
onChange={({ target }) => {
if (target.value.length <= 15) {
setNegativeText(target.value);
debounceNegativeDescription(target.value);
}
}}
onBlur={({ target }) => debounceNegativeDescription(target.value)}
sx={{
width: negativeTextWidth + 10 + "px",
background: "transparent",
fontSize: "18px",
minWidth: "95px",
maxWidth: "230px",
transition: "0.2s",
"& .MuiInputBase-root": {
"& .MuiInputBase-input": {
color: theme.palette.grey2.main,
fontSize: "16px",
padding: "0 3px",
borderRadius: "3px",
border: "1px solid",
borderColor: "transparent",
"&:hover, &:focus": {
borderColor: theme.palette.grey2.main,
<Box sx={{ minWidth: isMobile ? "140px" : "205px" }}>
<Typography
ref={negativeRef}
sx={{
position: "absolute",
opacity: 0,
zIndex: "-100",
whiteSpace: "nowrap",
fontSize: "16px",
}}
>
{negativeText}
</Typography>
<TextField
defaultValue={question.content.ratingNegativeDescription}
value={negativeText}
placeholder="Негативно"
onChange={({ target }) => {
if (target.value.length <= 15) {
setNegativeText(target.value);
debounceNegativeDescription(target.value);
}
}}
onBlur={({ target }) => debounceNegativeDescription(target.value)}
sx={{
width: negativeTextWidth + 10 + "px",
maxWidth: isMobile ? "140px" : "230px",
background: "transparent",
fontSize: "18px",
minWidth: "95px",
transition: "0.2s",
"& .MuiInputBase-root": {
"& .MuiInputBase-input": {
color: theme.palette.grey2.main,
fontSize: "16px",
padding: "0 3px",
borderRadius: "3px",
border: "1px solid",
borderColor: "transparent",
"&:hover, &:focus": {
borderColor: theme.palette.grey2.main,
},
},
"& .MuiOutlinedInput-notchedOutline": {
outline: "none",
border: "none",
},
},
"& .MuiOutlinedInput-notchedOutline": {
outline: "none",
border: "none",
},
},
}}
/>
<Typography
ref={positiveRef}
sx={{
position: "absolute",
opacity: 0,
zIndex: "-100",
whiteSpace: "nowrap",
fontSize: "16px",
}}
>
{positiveText}
</Typography>
<TextField
value={positiveText}
placeholder="Позитивно"
onChange={({ target }) => {
if (target.value.length <= 15) {
setPositiveText(target.value);
debouncePositiveDescription(target.value);
}
}}
onBlur={({ target }) => debouncePositiveDescription(target.value)}
sx={{
width: positiveTextWidth + 10 + "px",
background: "transparent",
fontSize: "18px",
minWidth: "95px",
maxWidth: "230px",
transition: "0.2s",
"& .MuiInputBase-root": {
"& .MuiInputBase-input": {
color: theme.palette.grey2.main,
fontSize: "16px",
padding: "0 3px",
borderRadius: "3px",
border: "1px solid",
borderColor: "transparent",
"&:hover, &:focus": {
borderColor: theme.palette.grey2.main,
}}
/>
</Box>
<Box sx={{ minWidth: isMobile ? "140px" : "205px" }}>
<Typography
ref={positiveRef}
sx={{
position: "absolute",
opacity: 0,
zIndex: "-100",
whiteSpace: "nowrap",
fontSize: "16px",
}}
>
{positiveText}
</Typography>
<TextField
value={positiveText}
placeholder="Позитивно"
onChange={({ target }) => {
if (target.value.length <= 15) {
setPositiveText(target.value);
debouncePositiveDescription(target.value);
}
}}
onBlur={({ target }) => debouncePositiveDescription(target.value)}
sx={{
width: positiveTextWidth + 10 + "px",
maxWidth: isMobile ? "140px" : "230px",
background: "transparent",
fontSize: "18px",
minWidth: "95px",
transition: "0.2s",
"& .MuiInputBase-root": {
"& .MuiInputBase-input": {
color: theme.palette.grey2.main,
fontSize: "16px",
padding: "0 3px",
borderRadius: "3px",
border: "1px solid",
borderColor: "transparent",
"&:hover, &:focus": {
borderColor: theme.palette.grey2.main,
},
},
"& .MuiOutlinedInput-notchedOutline": {
outline: "none",
border: "none",
},
},
"& .MuiOutlinedInput-notchedOutline": {
outline: "none",
border: "none",
},
},
}}
/>
}}
/>
</Box>
</Box>
</Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
<ButtonsOptions
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<SwitchRating switchState={switchState} totalIndex={totalIndex} />
</>
);

@ -16,6 +16,7 @@ import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon";
import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini";
import type { ButtonRatingFrom } from "./RatingOptions";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
type SettingSliderProps = {
totalIndex: number;
@ -28,10 +29,11 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
const buttonRatingForm: ButtonRatingFrom[] = [
@ -70,43 +72,41 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
Настройки рейтинга
</Typography>
<Box>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "16px",
fontWeight: 400,
mb: "8px",
}}
>
Форма
</Typography>
<Box sx={{ display: "flex" }}>
{buttonRatingForm.map(({ name, icon }, index) => (
<ButtonBase
key={index}
onClick={() => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.form = name;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
});
}}
sx={{
backgroundColor:
listQuestions[quizId][totalIndex].content.form === name
? theme.palette.brightPurple.main
: "transparent",
color: listQuestions[quizId][totalIndex].content.form === name ? "#ffffff" : theme.palette.grey3.main,
width: "40px",
height: "40px",
borderRadius: "4px",
}}
>
{icon}
</ButtonBase>
))}
</Box>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "16px",
fontWeight: 400,
}}
>
Форма
</Typography>
<Box sx={{ display: "flex", marginBottom: "15px" }}>
{buttonRatingForm.map(({ name, icon }, index) => (
<ButtonBase
key={index}
onClick={() => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, form: name },
});
}}
sx={{
backgroundColor:
question.content.form === name
? theme.palette.brightPurple.main
: "transparent",
color:
question.content.form === name
? "#ffffff"
: theme.palette.grey3.main,
width: "40px",
height: "40px",
borderRadius: "4px",
}}
>
{icon}
</ButtonBase>
))}
</Box>
<Typography
@ -119,24 +119,22 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
Количество
</Typography>
<Slider
value={listQuestions[quizId][totalIndex].content.steps}
value={question.content.steps}
min={2}
max={10}
aria-label="Default"
valueLabelDisplay="auto"
sx={{ color: theme.palette.brightPurple.main, padding: "0" }}
onChange={(_, value) => {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
steps: Number(value) || 1,
},
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, steps: Number(value) || 1 },
});
}}
/>
</Box>
<Box
sx={{
boxSizing: "border-box",
pt: isMobile ? "30px" : "20px",
pb: "20px",
pl: isFigmaTablte ? (isMobile ? "20px" : "34px") : "40px",
@ -153,9 +151,9 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
checked={!question.required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
required: !e.target.checked,
});
}}
@ -170,16 +168,15 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
@ -188,10 +185,10 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -6,6 +6,8 @@ import CustomNumberField from "@ui_kit/CustomNumberField";
import SwitchSlider from "./switchSlider";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
interface Props {
totalIndex: number;
}
@ -18,6 +20,7 @@ export default function SliderOptions({ totalIndex }: Props) {
const [stepError, setStepError] = useState("");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber;
const SSHC = (data: string) => {
setSwitchState(data);
@ -31,59 +34,60 @@ export default function SliderOptions({ totalIndex }: Props) {
maxWidth: "673.8px",
display: "flex",
pl: "20px",
pr: "20px",
pr: isMobile ? "13px" : "20px",
pb: isMobile ? "30px" : "20px",
flexDirection: "column",
gap: "20px",
gap: isMobile ? "25px" : "20px",
}}
>
<Box sx={{ gap: "14px", display: "flex", flexDirection: "column" }}>
<Box
sx={{
gap: isMobile ? "10px" : "14px",
mt: isMobile ? "25px" : "0px",
display: "flex",
flexDirection: "column",
marginRight: isMobile ? "10px" : "0px",
}}
>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: "#4D4D4D" }}>
Выбор значения из диапазона
</Typography>
<Box sx={{ width: "100%", display: "flex", alignItems: "center", gap: "20px" }}>
<Box sx={{ width: "100%", display: "flex", alignItems: "center", gap: isMobile ? "9px" : "20px" }}>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
placeholder={"0"}
min={0}
max={99}
value={
listQuestions[quizId][totalIndex].content.range.split("—")[0]
}
value={question.content.range.split("—")[0]}
onChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.range = `${target.value}${
listQuestions[quizId][totalIndex].content.range.split("—")[1]
}`;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: {
...question.content,
range: `${target.value}${
question.content.range.split("—")[1]
}`,
},
});
}}
onBlur={({ target }) => {
const start = listQuestions[quizId][totalIndex].content.start;
const start = question.content.start;
const min = Number(target.value);
const max = Number(
listQuestions[quizId][totalIndex].content.range.split("—")[1]
);
const max = Number(question.content.range.split("—")[1]);
if (min >= max) {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.range = `${max - 1 >= 0 ? max - 1 : 0}${
listQuestions[quizId][totalIndex].content.range.split(
"—"
)[1]
}`;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: {
...question.content,
range: `${max - 1 >= 0 ? max - 1 : 0}${
question.content.range.split("—")[1]
}`,
},
});
}
if (start < min) {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
start: min,
},
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, start: min },
});
}
}}
@ -94,62 +98,48 @@ export default function SliderOptions({ totalIndex }: Props) {
placeholder={"100"}
min={0}
max={100}
value={
listQuestions[quizId][totalIndex].content.range.split("—")[1]
}
value={question.content.range.split("—")[1]}
onChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.range = `${
listQuestions[quizId][totalIndex].content.range.split("—")[0]
}${target.value}`;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: {
...question.content,
range: `${question.content.range.split("—")[0]}${
target.value
}`,
},
});
}}
onBlur={({ target }) => {
const start = listQuestions[quizId][totalIndex].content.start;
const step = listQuestions[quizId][totalIndex].content.step;
const min = Number(
listQuestions[quizId][totalIndex].content.range.split("—")[0]
);
const start = question.content.start;
const step = question.content.step;
const min = Number(question.content.range.split("—")[0]);
const max = Number(target.value);
const range = max - min;
if (max <= min) {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.range = `${
listQuestions[quizId][totalIndex].content.range.split(
"—"
)[0]
}${min + 1 >= 100 ? 100 : min + 1}`;
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: {
...question.content,
range: `${question.content.range.split("—")[0]}${
min + 1 >= 100 ? 100 : min + 1
}`,
},
});
}
if (start > max) {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
start: max,
},
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, start: max },
});
}
if (step > max) {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
step: max,
},
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, step: max },
});
if (range % step) {
setStepError(
`Шаг должен делить без остатка диапазон ${max} - ${min} = ${
max - min
}`
);
setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`);
} else {
setStepError("");
}
@ -168,24 +158,18 @@ export default function SliderOptions({ totalIndex }: Props) {
}}
>
<Box sx={{ width: "100%" }}>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: "#4D4D4D", mb: "14px" }}>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: "#4D4D4D", mb: isMobile ? "10px" : "14px" }}>
Начальное значение
</Typography>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
placeholder={"50"}
min={Number(
listQuestions[quizId][totalIndex].content.range.split("—")[0]
)}
max={Number(
listQuestions[quizId][totalIndex].content.range.split("—")[1]
)}
value={String(listQuestions[quizId][totalIndex].content.start)}
min={Number(question.content.range.split("—")[0])}
max={Number(question.content.range.split("—")[1])}
value={String(question.content.start)}
onChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.start = Number(target.value);
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, start: Number(target.value) },
});
}}
/>
@ -207,39 +191,26 @@ export default function SliderOptions({ totalIndex }: Props) {
max={100}
placeholder={"1"}
error={stepError}
value={String(listQuestions[quizId][totalIndex].content.step)}
value={String(question.content.step)}
onChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.step = Number(target.value);
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, step: Number(target.value) },
});
}}
onBlur={({ target }) => {
const min = Number(
listQuestions[quizId][totalIndex].content.range.split("—")[0]
);
const max = Number(
listQuestions[quizId][totalIndex].content.range.split("—")[1]
);
const min = Number(question.content.range.split("—")[0]);
const max = Number(question.content.range.split("—")[1]);
const range = max - min;
const step = Number(target.value);
if (step > max) {
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
step: max,
},
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, step: max },
});
}
if (range % step) {
setStepError(
`Шаг должен делить без остатка диапазон ${max} - ${min} = ${
max - min
}`
);
setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`);
} else {
setStepError("");
}
@ -248,11 +219,7 @@ export default function SliderOptions({ totalIndex }: Props) {
</Box>
</Box>
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
<SwitchSlider switchState={switchState} totalIndex={totalIndex} />
</>
);

@ -6,6 +6,8 @@ import CustomTextField from "@ui_kit/CustomTextField";
import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
type SettingSliderProps = {
totalIndex: number;
};
@ -17,10 +19,11 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
return (
@ -34,25 +37,29 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
pb: isMobile ? "30px" : "20px",
pl: "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
width: isMobile ? "auto" : "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
Настройки ползунка
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
sx={{
mr: isMobile ? "0px" : "16px",
height: isMobile ? "100%" : "auto",
alignItems: isMobile ? "flex-start" : "center",
}}
label={"Выбор диапозона (два ползунка)"}
checked={listQuestions[quizId][totalIndex].content.chooseRange}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.chooseRange = e.target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.chooseRange}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, chooseRange: target.checked },
});
}}
/>
</Box>
@ -64,19 +71,19 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
pr: isFigmaTablte ? (isMobile ? "20px" : "12px") : "20px",
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
width: "100%",
gap: "14px",
width: isMobile ? "auto" : "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
sx={{ mr: isMobile ? "0px" : "16px", alignItems: isMobile ? "flex-end" : "center" }}
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
checked={!question.required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
required: !e.target.checked,
});
}}
@ -85,22 +92,25 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
alignItems: "flex-start",
}}
>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px", whiteSpace: isFigmaTablte ? "nowrap" : "" }}
sx={{
mr: isMobile ? "0px" : "16px",
height: isMobile ? "100%" : "auto",
alignItems: isMobile ? "flex-start" : "center",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
@ -109,10 +119,10 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -13,7 +13,18 @@ import RatingIcon from "../../assets/icons/questionsPage/rating";
import { Box } from "@mui/material";
import React from "react";
import { useParams } from "react-router-dom";
import { questionStore, updateQuestionsList } from "@root/questions";
import {
questionStore,
updateQuestionsList,
createQuestion,
removeQuestionForce,
} from "@root/questions";
import type {
QuizQuestionType,
QuizQuestionBase,
} from "../../model/questionTypes/shared";
interface Props {
totalIndex: number;
@ -22,7 +33,7 @@ interface Props {
type ButtonTypeQuestion = {
icon: JSX.Element;
title: string;
value: string;
value: QuizQuestionType;
};
export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
@ -86,7 +97,6 @@ export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
export default function TypeQuestions({ totalIndex }: Props) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const switchState = listQuestions[quizId][totalIndex].type;
return (
<Box
@ -101,7 +111,14 @@ export default function TypeQuestions({ totalIndex }: Props) {
<QuestionsMiniButton
key={title}
onClick={() => {
updateQuestionsList(quizId, totalIndex, { type: value });
const question = { ...listQuestions[quizId][totalIndex] };
removeQuestionForce(quizId, question.id);
createQuestion(quizId, value, totalIndex);
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
expanded: question.expanded,
type: value,
});
}}
icon={icon}
text={title}

@ -7,6 +7,7 @@ import {
Select,
SelectChangeEvent,
Typography,
Tooltip,
useMediaQuery,
useTheme,
} from "@mui/material";
@ -18,11 +19,22 @@ import InfoIcon from "../../../assets/icons/InfoIcon";
import ArrowDown from "../../../assets/icons/ArrowDownIcon";
import SwitchUpload from "./switchUpload";
import type { AnyQuizQuestion } from "../../../model/questionTypes/shared";
import type {
QuizQuestionFile,
UploadFileType,
} from "../../../model/questionTypes/file";
interface Props {
totalIndex: number;
}
const DESIGN_TYPES = [
type DesignItem = {
name: string;
value: UploadFileType;
};
const DESIGN_TYPES: DesignItem[] = [
{ name: "Все типы файлов", value: "all" },
{ name: "Изображения", value: "picture" },
{ name: "Видео", value: "video" },
@ -36,6 +48,7 @@ export default function UploadFile({ totalIndex }: Props) {
const { listQuestions } = questionStore();
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(980));
const question = listQuestions[quizId][totalIndex] as QuizQuestionFile;
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -44,18 +57,20 @@ export default function UploadFile({ totalIndex }: Props) {
};
const handleChange = ({ target }: SelectChangeEvent) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.type = target.value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<AnyQuizQuestion>(quizId, totalIndex, {
content: { ...question.content, type: target.value as UploadFileType },
});
};
useEffect(() => {
const isTypeSetted = DESIGN_TYPES.find(({ value }) => value === listQuestions[quizId][totalIndex].content.type);
const isTypeSetted = DESIGN_TYPES.find(
({ value }) => value === question.content.type
);
if (!isTypeSetted) {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.type = DESIGN_TYPES[0].value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<AnyQuizQuestion>(quizId, totalIndex, {
content: { ...question.content, type: DESIGN_TYPES[0].value },
});
}
}, []);
@ -70,7 +85,7 @@ export default function UploadFile({ totalIndex }: Props) {
flexDirection: "column",
}}
>
<Box sx={{ gap: "10px", display: "flex" }}>
<Box sx={{ gap: "10px", display: "flex", mt: isMobile ? "15px" : "0px" }}>
<FormControl
fullWidth
size="small"
@ -84,7 +99,7 @@ export default function UploadFile({ totalIndex }: Props) {
<Select
id="category-select"
variant="outlined"
value={listQuestions[quizId][totalIndex].content.type}
value={question.content.type}
displayEmpty
onChange={handleChange}
sx={{
@ -151,9 +166,9 @@ export default function UploadFile({ totalIndex }: Props) {
<Box
sx={{
display: "flex",
alignItems: "center",
alignItems: "flex-start",
marginBottom: "20px",
marginTop: isMobile ? "18px" : "15px",
marginTop: "15px",
}}
>
<Typography
@ -166,10 +181,21 @@ export default function UploadFile({ totalIndex }: Props) {
>
Пользователь может загрузить любой собственный файл
</Typography>
<InfoIcon />
<Tooltip
title="Можно загрузить файл в желаемом формате."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
</Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
<ButtonsOptions
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<SwitchUpload switchState={switchState} totalIndex={totalIndex} />
</>
);

@ -1,5 +1,11 @@
import { useParams } from "react-router-dom";
import { Box, Typography, useMediaQuery, useTheme, Tooltip } from "@mui/material";
import {
Box,
Typography,
useMediaQuery,
useTheme,
Tooltip,
} from "@mui/material";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce";
@ -8,6 +14,8 @@ import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionFile } from "../../../model/questionTypes/file";
type SettingsUploadProps = {
totalIndex: number;
};
@ -16,75 +24,95 @@ export default function SettingsUpload({ totalIndex }: SettingsUploadProps) {
const theme = useTheme();
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionFile;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
const isMobile = useMediaQuery(theme.breakpoints.down(790));
return (
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
boxSizing: "border-box",
pt: isMobile ? "30px" : "20px",
pb: "20px",
pl: "20px",
pr: isMobile ? "20px" : "0px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
width: isMobile ? "auto" : "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
Настройки вопроса
</Typography>
<Typography>Настройки вопроса</Typography>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
}}
label={"Автозаполнение адреса"}
checked={listQuestions[quizId][totalIndex].content.autofill}
checked={question.content.autofill}
handleChange={({ target }) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.autofill = target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, {
content: { ...question.content, autofill: target.checked },
});
}}
/>
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
}}
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
checked={!question.required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, {
required: !e.target.checked,
});
}}
/>
<Box sx={{ width: isMobile ? "90%" : "auto", display: "flex", alignItems: "center" }}>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
sx={{
mr: isMobile ? "0px" : "16px",
height: isMobile ? "100%" : "26px",
alignItems: "flex-start",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
sx={{ paddingRight: "20px" }}
/>
)}
</Box>

@ -9,6 +9,7 @@ import { questionStore, updateQuestionsList } from "@root/questions";
import { UploadImageModal } from "./UploadImageModal";
import type { DragEvent } from "react";
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
type UploadImageProps = {
totalIndex: number;
@ -19,6 +20,7 @@ export default function UploadImage({ totalIndex }: UploadImageProps) {
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
@ -26,9 +28,12 @@ export default function UploadImage({ totalIndex }: UploadImageProps) {
if (files?.length) {
const [file] = Array.from(files);
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.back = URL.createObjectURL(file);
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: {
...question.content,
back: URL.createObjectURL(file),
},
});
handleClose();
setOpened(true);
@ -70,7 +75,7 @@ export default function UploadImage({ totalIndex }: UploadImageProps) {
<CropModal
opened={opened}
onClose={() => setOpened(false)}
picture={listQuestions[quizId][totalIndex].content.back}
picture={question.content.back}
/>
</Box>
);

@ -7,6 +7,8 @@ import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
interface Props {
totalIndex: number;
}
@ -15,7 +17,7 @@ export default function AnswerOptions({ totalIndex }: Props) {
const [switchState, setSwitchState] = useState("setting");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const variants = listQuestions[quizId][totalIndex].content.variants;
const question = listQuestions[quizId][totalIndex] as QuizQuestionVariant;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -24,21 +26,18 @@ export default function AnswerOptions({ totalIndex }: Props) {
};
const addNewAnswer = () => {
const answerNew = variants.slice();
answerNew.push({ answer: "", hints: "", emoji: "", image: "" });
const answerNew = question.content.variants.slice();
answerNew.push({ answer: "", hints: "", extendedText: "" });
updateQuestionsList(quizId, totalIndex, {
content: {
...listQuestions[quizId][totalIndex].content,
variants: answerNew,
},
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
return (
<>
<Box sx={{ padding: "0 20px 0px 20px" }}>
{variants.length === 0 ? (
<Box sx={{ padding: "0 20px 20px 20px" }}>
{question.content.variants.length === 0 ? (
<Typography
sx={{
padding: "0 0 33px 80px",
@ -51,7 +50,10 @@ export default function AnswerOptions({ totalIndex }: Props) {
Добавьте ответ
</Typography>
) : (
<AnswerDraggableList variants={variants} totalIndex={totalIndex} />
<AnswerDraggableList
variants={question.content.variants}
totalIndex={totalIndex}
/>
)}
<Box
@ -92,11 +94,7 @@ export default function AnswerOptions({ totalIndex }: Props) {
)}
</Box>
</Box>
<ButtonsOptionsAndPict
switchState={switchState}
SSHC={SSHC}
totalIndex={totalIndex}
/>
<ButtonsOptionsAndPict switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
<SwitchAnswerOptions switchState={switchState} totalIndex={totalIndex} />
</>
);

@ -1,11 +1,19 @@
import { useParams } from "react-router-dom";
import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material";
import {
Box,
Typography,
Tooltip,
useMediaQuery,
useTheme,
} from "@mui/material";
import { useDebouncedCallback } from "use-debounce";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import InfoIcon from "../../../assets/icons/InfoIcon";
import CustomTextField from "@ui_kit/CustomTextField";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
interface Props {
totalIndex: number;
}
@ -15,12 +23,14 @@ export default function ResponseSettings({ totalIndex }: Props) {
const { listQuestions } = questionStore();
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionVariant;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerName = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
return (
@ -43,102 +53,124 @@ export default function ResponseSettings({ totalIndex }: Props) {
width: "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки ответов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Длинный текстовый ответ"}
checked={listQuestions[quizId][totalIndex].content.largeCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.largeCheck = e.target.checked;
if (!e.target.checked) {
clonContent.large = "";
}
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.largeCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: {
...question.content,
largeCheck: target.checked,
},
});
}}
/>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={listQuestions[quizId][totalIndex].content.multi}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.multi = e.target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.multi}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: { ...question.content, multi: target.checked },
});
}}
/>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={listQuestions[quizId][totalIndex].content.own}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.own = e.target.checked;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
checked={question.content.own}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: { ...question.content, own: target.checked },
});
}}
/>
</Box>
<Box
sx={{
boxSizing: "border-box",
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isFigmaTablte ? (isMobile ? "20px" : "34px") : "28px",
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
pr: isFigmaTablte ? "19px" : "20px",
display: "flex",
flexDirection: "column",
gap: isMobile ? "13px" : "14px",
gap: "14px",
width: "100%",
}}
>
<Typography sx={{ height: isMobile ? "18px" : "auto", fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!listQuestions[quizId][totalIndex].required}
handleChange={(e) => {
updateQuestionsList(quizId, totalIndex, {
required: !e.target.checked,
checked={!question.required}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
required: !target.checked,
});
}}
/>
<Box sx={{ width: isMobile ? "90%" : "auto", display: "flex" }}>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "9px", height: isMobile ? "42px" : "26px", alignItems: "start" }}
sx={{
mr: isMobile ? "0px" : "9px",
height: isMobile ? "42px" : "26px",
alignItems: "start",
}}
label={"Внутреннее название вопроса"}
checked={listQuestions[quizId][totalIndex].content.innerNameCheck}
handleChange={(e) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.innerNameCheck = e.target.checked;
if (!e.target.checked) {
clonContent.innerName = "";
}
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
content: {
...question.content,
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/>
{isMobile ||
(!isFigmaTablte && (
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
<Box>
<InfoIcon />
</Box>
</Tooltip>
))}
{isMobile && (
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
{listQuestions[quizId][totalIndex].content.innerNameCheck && (
{question.content.innerNameCheck && (
<CustomTextField
sx={{ mr: isMobile ? "0px" : "16px" }}
placeholder={"Развёрнутое описание вопроса"}
text={listQuestions[quizId][totalIndex].content.innerName}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
/>
)}

@ -11,10 +11,15 @@ import {
Modal,
Radio,
RadioGroup,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { questionStore, resetSomeField, updateQuestionsList } from "@root/questions";
import {
questionStore,
resetSomeField,
updateQuestionsList,
} from "@root/questions";
import { Select } from "./Select";
import RadioCheck from "@ui_kit/RadioCheck";
@ -22,6 +27,8 @@ import RadioIcon from "@ui_kit/RadioIcon";
import InfoIcon from "@icons/Info";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import type { QuizQuestionBase } from "../../model/questionTypes/shared";
type BranchingQuestionsProps = {
totalIndex: number;
};
@ -29,15 +36,21 @@ type BranchingQuestionsProps = {
const ACTIONS = ["Показать", "Скрыть"];
const STIPULATIONS = ["Условие 1", "Условие 2", "Условие 3"];
const ANSWERS = ["Ответ 1", "Ответ 2", "Ответ 3"];
const CONDITIONS = ["Все условия обязательны", "Обязательно хотя бы одно условие"];
const CONDITIONS = [
"Все условия обязательны",
"Обязательно хотя бы одно условие",
];
export default function BranchingQuestions({ totalIndex }: BranchingQuestionsProps) {
export default function BranchingQuestions({
totalIndex,
}: BranchingQuestionsProps) {
const theme = useTheme();
const [title, setTitle] = useState<string>("");
const [titleInputWidth, setTitleInputWidth] = useState<number>(0);
const quizId = Number(useParams().quizId);
const { openedModalSettings, listQuestions } = questionStore();
const titleRef = useRef<HTMLDivElement>(null);
const [title, setTitle] = useState<string>(listQuestions[quizId][totalIndex].title)
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
useEffect(() => {
setTitleInputWidth(titleRef.current?.offsetWidth || 0);
@ -74,9 +87,10 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
sx={{
boxSizing: "border-box",
background: "#F2F3F7",
padding: "25px",
height: "70px",
padding: "0 25px",
display: "flex",
alignItems: "center",
}}
>
<Box sx={{ color: "#9A9AAF" }}>
@ -113,7 +127,14 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
</Box>
<Typography component="span">)</Typography>
</Box>
<InfoIcon width={24} height={24} />
<Tooltip
title="Настройте условия, при которых данный вопрос будет отображаться в квизе."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
<Box
sx={{
@ -132,19 +153,25 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
>
<Select
items={ACTIONS}
activeItemIndex={listQuestions[quizId][totalIndex].content.rule.show ? 0 : 1}
activeItemIndex={question.content.rule.show ? 0 : 1}
sx={{ maxWidth: "140px" }}
onChange={(action) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.rule.show = action === ACTIONS[0];
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
content: {
...question.content,
rule: {
...question.content.rule,
show: action === ACTIONS[0],
},
},
});
}}
/>
<Typography sx={{ color: theme.palette.grey2.main }}>если в ответе на вопрос</Typography>
<Typography sx={{ color: theme.palette.grey2.main }}>
если в ответе на вопрос
</Typography>
</Box>
{listQuestions[quizId][totalIndex].content.rule.reqs.map((request, index) => (
{question.content.rule.reqs.map((request, index) => (
<Box
key={index}
sx={{
@ -162,19 +189,24 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Условие 1</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Условие 1
</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.rule.reqs.splice(index, 1);
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
});
const clonedContent = { ...question.content };
clonedContent.rule.reqs.splice(index, 1);
updateQuestionsList<QuizQuestionBase>(
quizId,
totalIndex,
{
content: clonedContent,
}
);
}}
>
<DeleteIcon style={{ color: "#4D4D4D" }} />
<DeleteIcon color={"#4D4D4D"} />
</IconButton>
</Box>
<Select
@ -182,15 +214,19 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
activeItemIndex={request.id ? Number(request.id) : -1}
items={STIPULATIONS}
onChange={(stipulation) => {
const clonContent = listQuestions[quizId][totalIndex].content;
const clonedContent = { ...question.content };
clonContent.rule.reqs[index] = {
id: String(STIPULATIONS.findIndex((item) => item.includes(stipulation))),
clonedContent.rule.reqs[index] = {
id: String(
STIPULATIONS.findIndex((item) =>
item.includes(stipulation)
)
),
vars: request.vars,
};
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
content: clonedContent,
});
}}
sx={{ marginBottom: "15px" }}
@ -204,7 +240,9 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
pb: "10px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Дан ответ</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Дан ответ
</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
(Укажите один или несколько вариантов)
</Typography>
@ -214,16 +252,28 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
activeItemIndex={-1}
items={ANSWERS}
onChange={(answer) => {
const clonContent = listQuestions[quizId][totalIndex].content;
const answerItemIndex = ANSWERS.findIndex((answerItem) => answerItem === answer);
const clonedContent = { ...question.content };
const answerItemIndex = ANSWERS.findIndex(
(answerItem) => answerItem === answer
);
if (!clonContent.rule.reqs[index].vars.includes(answerItemIndex)) {
listQuestions[quizId][totalIndex].content.rule.reqs[index].vars.push(answerItemIndex);
if (
!clonedContent.rule.reqs[index].vars.includes(
answerItemIndex
)
) {
question.content.rule.reqs[index].vars.push(
answerItemIndex
);
}
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
});
updateQuestionsList<QuizQuestionBase>(
quizId,
totalIndex,
{
content: clonedContent,
}
);
}}
sx={{
marginBottom: "10px",
@ -238,25 +288,34 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
gap: "10px",
}}
>
{listQuestions[quizId][totalIndex].content.rule.reqs[index].vars.map((item, varIndex) => (
<Chip
key={varIndex}
label={ANSWERS[item]}
variant="outlined"
onDelete={() => {
const clonContent = listQuestions[quizId][totalIndex].content;
const removedItemIndex = clonContent.rule.reqs[index].vars.findIndex(
(varItem) => varItem === item
);
{question.content.rule.reqs[index].vars.map(
(item, varIndex) => (
<Chip
key={varIndex}
label={ANSWERS[item]}
variant="outlined"
onDelete={() => {
const clonedContent = { ...question.content };
const removedItemIndex = clonedContent.rule.reqs[
index
].vars.findIndex((varItem) => varItem === item);
clonContent.rule.reqs[index].vars.splice(removedItemIndex, 1);
clonedContent.rule.reqs[index].vars.splice(
removedItemIndex,
1
);
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
});
}}
/>
))}
updateQuestionsList<QuizQuestionBase>(
quizId,
totalIndex,
{
content: clonedContent,
}
);
}}
/>
)
)}
</Box>
</>
)}
@ -276,10 +335,10 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
marginBottom: "10px",
}}
onClick={() => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.rule.reqs.push({ id: "", vars: [] });
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
const clonedContent = { ...question.content };
clonedContent.rule.reqs.push({ id: "", vars: [] });
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
content: clonedContent,
});
}}
>
@ -288,12 +347,16 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
<FormControl>
<RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group"
value={listQuestions[quizId][totalIndex].content.rule.or ? 1 : 0}
value={question.content.rule.or ? 1 : 0}
onChange={(_, value) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.rule.or = Boolean(Number(value));
updateQuestionsList(quizId, totalIndex, {
content: clonContent,
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
content: {
...question.content,
rule: {
...question.content.rule,
or: Boolean(Number(value)),
},
},
});
}}
>
@ -302,7 +365,12 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
key={index}
sx={{ color: theme.palette.grey2.main }}
value={index}
control={<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />}
control={
<Radio
checkedIcon={<RadioCheck />}
icon={<RadioIcon />}
/>
}
label={condition}
/>
))}
@ -310,10 +378,18 @@ export default function BranchingQuestions({ totalIndex }: BranchingQuestionsPro
</FormControl>
</Box>
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}>
<Button variant="outlined" onClick={handleClose} sx={{ width: "100%", maxWidth: "130px" }}>
<Button
variant="outlined"
onClick={handleClose}
sx={{ width: "100%", maxWidth: "130px" }}
>
Отмена
</Button>
<Button variant="contained" sx={{ width: "100%", maxWidth: "130px" }} onClick={handleClose}>
<Button
variant="contained"
sx={{ width: "100%", maxWidth: "130px" }}
onClick={handleClose}
>
Готово
</Button>
</Box>

@ -9,6 +9,8 @@ import UploadBox from "@ui_kit/UploadBox";
import { questionStore, updateQuestionsList } from "@root/questions";
import { UploadVideoModal } from "./UploadVideoModal";
import type { QuizQuestionBase } from "../../model/questionTypes/shared";
type BackgroundType = "text" | "video";
type HelpQuestionsProps = {
@ -20,16 +22,25 @@ export default function HelpQuestions({ totalIndex }: HelpQuestionsProps) {
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
const debounced = useDebouncedCallback((value) => {
let clonContent = listQuestions[quizId][totalIndex].content;
clonContent.hint.text = value;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
content: {
...question.content,
hint: { text: value, video: question.content.hint.video },
},
});
}, 1000);
const videoHC = (url: string) => {
const clonContent = listQuestions[quizId][totalIndex].content;
clonContent.hint.video = url;
updateQuestionsList(quizId, totalIndex, { content: clonContent });
const clonedContent = { ...question.content };
clonedContent.hint.video = url;
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
content: {
...question.content,
hint: { video: url, text: question.content.hint.text },
},
});
};
return (
@ -67,7 +78,7 @@ export default function HelpQuestions({ totalIndex }: HelpQuestionsProps) {
<>
<CustomTextField
placeholder={"Текст консультанта"}
text={listQuestions[quizId][totalIndex].content.hint.text}
text={question.content.hint.text}
onChange={({ target }) => debounced(target.value)}
/>
</>
@ -80,12 +91,8 @@ export default function HelpQuestions({ totalIndex }: HelpQuestionsProps) {
onClick={() => setOpen(true)}
sx={{ justifyContent: "flex-start" }}
>
{listQuestions[quizId][totalIndex].content.hint.video ? (
<video
src={listQuestions[quizId][totalIndex].content.hint.video}
width="400"
controls
/>
{question.content.hint.video ? (
<video src={question.content.hint.video} width="400" controls />
) : (
<>
<UploadBox
@ -101,7 +108,7 @@ export default function HelpQuestions({ totalIndex }: HelpQuestionsProps) {
<UploadVideoModal
open={open}
onClose={() => setOpen(false)}
video={listQuestions[quizId][totalIndex].content.hint.video}
video={question.content.hint.video}
onUpload={videoHC}
/>
</Box>

@ -1,18 +1,18 @@
import {Link, useParams} from "react-router-dom";
import {Box, Button, useTheme} from "@mui/material";
import { Link, useParams } from "react-router-dom";
import { Box, Button, useTheme } from "@mui/material";
import CreationFullCard from "./CreationFullCard";
import Info from "../../assets/icons/Info";
import image from "../../assets/Rectangle 110.png";
import {quizStore} from "@root/quizes";
import { quizStore } from "@root/quizes";
export const Result = () => {
const {listQuizes, updateQuizesList} = quizStore();
const params = Number(useParams().quizId);
const handleNext = () => {
updateQuizesList(params, {step: listQuizes[params].step + 1})
}
const { listQuizes, updateQuizesList } = quizStore();
const params = Number(useParams().quizId);
const handleNext = () => {
updateQuizesList(params, { createResult: true });
};
const theme = useTheme();
return (
<Box component="section">
@ -22,16 +22,16 @@ export const Result = () => {
image={image}
/>
<Box sx={{ mt: "30px", alignItems: "center" }}>
<Button
variant="contained"
sx={{
mr: "23px",
minWidth: "258px",
}}
onClick={handleNext}
>
Создать результаты
</Button>
<Button
variant="contained"
sx={{
mr: "23px",
minWidth: "258px",
}}
onClick={handleNext}
>
Создать результаты
</Button>
<Info />
</Box>

@ -35,7 +35,7 @@ export default function FirstQuiz() {
<Button
variant="contained"
onClick={() => {
navigate(`/quize-setting/${createBlank()}`);
navigate(`/setting/${createBlank()}`);
}}
>
Создать +

@ -40,7 +40,7 @@ export default function MyQuizzesFull({outerContainerSx: sx, children}: Props) {
variant="contained"
sx={{padding: "10px 47px"}}
onClick={() => {
navigate(`/quize-setting/${createBlank()}`);
navigate(`/setting/${createBlank()}`);
}}
>Создать +</Button>
</Box>
@ -62,7 +62,7 @@ export default function MyQuizzesFull({outerContainerSx: sx, children}: Props) {
removeQuiz(e.id)
}}
onClickEdit={() =>
navigate(`/quize-setting/${e.id}`)
navigate(`/setting/${e.id}`)
}
/>
)

@ -2,7 +2,17 @@ import Stepper from "@ui_kit/Stepper";
import SwitchStepPages from "@ui_kit/switchStepPages";
import React, { useState } from "react";
import PenaLogo from "@ui_kit/PenaLogo";
import { Box, Button, Container, FormControl, IconButton, TextField, useMediaQuery, useTheme } from "@mui/material";
import {
Box,
Button,
Container,
FormControl,
IconButton,
TextField,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Link } from "react-router-dom";
import BackArrowIcon from "@icons/BackArrowIcon";
import NavMenuItem from "@ui_kit/Header/NavMenuItem";
import EyeIcon from "@icons/EyeIcon";
@ -14,6 +24,15 @@ import { Burger } from "@icons/Burger";
import { PenaLogoIcon } from "@icons/PenaLogoIcon";
import { SidebarMobile } from "./Sidebar/SidebarMobile";
const DESCRIPTIONS = [
"Настройка стартовой страницы",
"Задайте вопросы",
"Настройте авторезультаты",
"Настройте форму контактов",
"Установите квиз",
"Запустите рекламу",
] as const;
export default function StartPage() {
const { listQuizes, updateQuizesList, removeQuiz, createBlank } = quizStore();
const params = Number(useParams().quizId);
@ -24,10 +43,6 @@ export default function StartPage() {
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false);
const handleNext = () => {
updateQuizesList(params, { step: listQuizes[params].step + 1 });
};
const handleBack = () => {
let result = listQuizes[params].step - 1;
updateQuizesList(params, { step: result ? result : 1 });
@ -50,7 +65,13 @@ export default function StartPage() {
zIndex: theme.zIndex.drawer + 1,
}}
>
{isMobile ? <PenaLogoIcon style={{ fontSize: "39px", color: "white" }} /> : <PenaLogo width={124} />}
<Link to="/" style={{ display: "flex" }}>
{isMobile ? (
<PenaLogoIcon style={{ fontSize: "39px", color: "white" }} />
) : (
<PenaLogo width={124} />
)}
</Link>
<Box
sx={{
display: isMobile ? "none" : "flex",
@ -104,7 +125,12 @@ export default function StartPage() {
/>
) : (
<CustomAvatar
sx={{ ml: "11px", backgroundColor: theme.palette.orange.main, height: "36px", width: "36px" }}
sx={{
ml: "11px",
backgroundColor: theme.palette.orange.main,
height: "36px",
width: "36px",
}}
/>
)}
</Box>
@ -157,7 +183,12 @@ export default function StartPage() {
Опубликовать
</Button>
<CustomAvatar
sx={{ ml: "11px", backgroundColor: theme.palette.orange.main, height: "36px", width: "36px" }}
sx={{
ml: "11px",
backgroundColor: theme.palette.orange.main,
height: "36px",
width: "36px",
}}
/>
</Box>
</>
@ -180,8 +211,16 @@ export default function StartPage() {
boxSizing: "border-box",
}}
>
<Stepper activeStep={activeStep} desc={"Настройка стартовой страницы"} />
<SwitchStepPages activeStep={activeStep} handleNext={handleNext} />
<Stepper
activeStep={activeStep}
desc={DESCRIPTIONS[activeStep - 1]}
/>
<SwitchStepPages
activeStep={activeStep}
quizType={listQuizes[params].config.type}
startpage={listQuizes[params].startpage}
createResult={listQuizes[params].createResult}
/>
</Box>
</Box>
</>

@ -36,10 +36,6 @@ import AlignCenterIcon from "@icons/AlignCenterIcon";
import DropFav from "./dropfavicon";
import { createQuestion } from "@root/questions";
interface HandleNext {
handleNext: () => void;
}
const designTypes = [
[
"standard",
@ -62,7 +58,7 @@ const designTypes = [
type BackgroundType = "image" | "video";
type AlignType = "left" | "right" | "center";
export default function StartPageSettings({ handleNext }: HandleNext) {
export default function StartPageSettings() {
const { listQuizes, updateQuizesList, removeQuiz, createBlank } = quizStore();
const params = Number(useParams().quizId);
const theme = useTheme();
@ -73,6 +69,10 @@ export default function StartPageSettings({ handleNext }: HandleNext) {
);
const [alignType, setAlignType] = useState<AlignType>("left");
const handleNext = () => {
updateQuizesList(params, { step: listQuizes[params].step + 1 });
};
const videoHC = (videoInp: HTMLInputElement) => {
const file = videoInp.files?.[0];

@ -2,19 +2,15 @@ import { Box, Button, Typography, useTheme } from "@mui/material";
import CreationCard from "@ui_kit/CreationCard";
import quizCreationImage1 from "../../assets/quiz-creation-1.png";
import quizCreationImage2 from "../../assets/quiz-creation-2.png";
import {useParams} from "react-router-dom";
import {quizStore} from "@root/quizes";
import { useParams } from "react-router-dom";
import { quizStore } from "@root/quizes";
interface HandleNext {
handleNext: () => void;
}
export default function StepOne({ handleNext }: HandleNext) {
export default function StepOne() {
const theme = useTheme();
const params = Number(useParams().quizId);
const {listQuizes, updateQuizesList,} = quizStore()
const { listQuizes, updateQuizesList } = quizStore();
return (
<>
<Box
@ -24,32 +20,45 @@ export default function StepOne({ handleNext }: HandleNext) {
mt: "60px",
}}
>
<Button variant="text" onClick={() => {
let SPageClone = listQuizes[params].config
SPageClone.type = "quize"
updateQuizesList(params, {config: SPageClone })
handleNext()
}
}>
<Button
variant="text"
onClick={() => {
let SPageClone = listQuizes[params].config;
SPageClone.type = "quize";
updateQuizesList(params, { config: SPageClone });
}}
>
<CreationCard
header="Создание квиз-опроса"
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
image={quizCreationImage1}
border={listQuizes[params].config.type === "quize" ? "1px solid #7E2AEA" : "none"}
border={
listQuizes[params].config.type === "quize"
? "1px solid #7E2AEA"
: "none"
}
/>
</Button>
<Button variant="text" onClick={() => {
let SPageClone = listQuizes[params].config
SPageClone.type = "form"
updateQuizesList(params, {config: SPageClone })
handleNext()
}
}>
<Button
variant="text"
onClick={() => {
let SPageClone = listQuizes[params].config;
SPageClone.type = "form";
updateQuizesList(params, {
config: SPageClone,
step: listQuizes[params].step + 1,
});
}}
>
<CreationCard
header="Создание анкеты"
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
image={quizCreationImage2}
border={listQuizes[params].config.type === "form" ? "1px solid #7E2AEA" : "none"}
border={
listQuizes[params].config.type === "form"
? "1px solid #7E2AEA"
: "none"
}
/>
</Button>
</Box>

@ -6,11 +6,7 @@ import cardImage3 from "../../assets/card-3.png";
import {quizStore} from "@root/quizes";
import {useParams} from "react-router-dom";
interface HandleNext {
handleNext: () => void
}
export default function Steptwo ({handleNext}:HandleNext) {
export default function Steptwo () {
const params = Number(useParams().quizId);
const {listQuizes, updateQuizesList} = quizStore()
return (
@ -27,7 +23,6 @@ export default function Steptwo ({handleNext}:HandleNext) {
<Button variant='text'
onClick={() => {
updateQuizesList(params, {startpage: "standard"})
handleNext()
}}
>
<CardWithImage image={cardImage1} text="Standard" border={listQuizes[params].startpage === "standard" ? "1px solid #7E2AEA" : "none"} />
@ -35,7 +30,6 @@ export default function Steptwo ({handleNext}:HandleNext) {
<Button variant='text'
onClick={() => {
updateQuizesList(params, {startpage: "expanded"})
handleNext()
}}
>
<CardWithImage image={cardImage2} text="Expanded" border={listQuizes[params].startpage === "expanded" ? "1px solid #7E2AEA" : "none"}/>
@ -43,7 +37,6 @@ export default function Steptwo ({handleNext}:HandleNext) {
<Button variant='text'
onClick={() => {
updateQuizesList(params, {startpage: "centered"})
handleNext()
}}
>
<CardWithImage image={cardImage3} text="Centered" border={listQuizes[params].startpage === "centered" ? "1px solid #7E2AEA" : "none"}/>

@ -1,154 +1,39 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export type Variants = {
answer: string;
hints: string;
emoji: string;
image: string;
};
import { QuizQuestionEmoji } from "../model/questionTypes/emoji";
import { QuizQuestionImages } from "../model/questionTypes/images";
import { QuizQuestionVariant } from "../model/questionTypes/variant";
import { QuizQuestionVarImg } from "../model/questionTypes/varimg";
type Hint = {
text: string;
video: string;
};
import type {
AnyQuizQuestion,
QuizQuestionType,
QuestionVariant,
} from "../model/questionTypes/shared";
type Rule = {
or: boolean;
show: boolean;
reqs: [
{
id: string;
vars: number[];
}
];
};
import { QUIZ_QUESTION_BASE } from "../constants/base";
import { QUIZ_QUESTION_DATE } from "../constants/date";
import { QUIZ_QUESTION_EMOJI } from "../constants/emoji";
import { QUIZ_QUESTION_FILE } from "../constants/file";
import { QUIZ_QUESTION_IMAGES } from "../constants/images";
import { QUIZ_QUESTION_NUMBER } from "../constants/number";
import { QUIZ_QUESTION_PAGE } from "../constants/page";
import { QUIZ_QUESTION_RATING } from "../constants/rating";
import { QUIZ_QUESTION_SELECT } from "../constants/select";
import { QUIZ_QUESTION_TEXT } from "../constants/text";
import { QUIZ_QUESTION_VARIANT } from "../constants/variant";
import { QUIZ_QUESTION_VARIMG } from "../constants/varimg";
import { setAutoFreeze } from "immer";
export interface Question {
id: number;
title: string;
description: string;
type: string;
required: boolean;
deleted: boolean;
deleteTimeoutId: number;
page: number;
content: {
variants: Variants[];
hint: Hint;
rule: Rule;
largeCheck: boolean;
large: string;
multi: boolean;
own: boolean;
innerNameCheck: boolean;
innerName: string;
back: string;
placeholder: string;
type: string;
autofill: boolean;
default: string;
single: boolean;
number: boolean;
xy: string;
format: "carousel" | "masonry";
text: string;
picture: string;
video: string;
dateRange: boolean;
time: boolean;
form: string;
steps: number;
range: string;
start: number;
step: number;
chooseRange: boolean;
required: boolean;
replText: string;
ratingNegativeDescription: string;
ratingPositiveDescription: string;
};
version: number;
parent_ids: number[];
created_at: string;
updated_at: string;
expanded: boolean;
}
setAutoFreeze(false);
interface QuestionStore {
listQuestions: Record<string, Question[]>;
listQuestions: Record<string, AnyQuizQuestion[]>;
openedModalSettings: string;
}
export const DEFAULT_QUESTION: Omit<Question, "id"> = {
title: "",
description: "",
type: "",
required: true,
deleted: false,
deleteTimeoutId: 0,
page: 0,
content: {
largeCheck: false,
large: "",
multi: false,
own: false,
innerNameCheck: false,
innerName: "",
back: "",
placeholder: "",
type: "all",
autofill: true,
default: "",
number: false,
single: false,
xy: "",
format: "carousel",
text: "",
picture: "",
video: "",
dateRange: false,
time: false,
form: "star",
steps: 5,
range: "0—100",
start: 50,
step: 1,
chooseRange: false,
required: false,
replText: "",
ratingNegativeDescription: "",
ratingPositiveDescription: "",
variants: [
{
answer: "",
hints: "",
emoji: "",
image: "",
},
],
hint: {
text: "",
video: "",
},
rule: {
or: true,
show: true,
reqs: [
{
id: "",
vars: [],
},
],
},
},
version: 0,
parent_ids: [0],
created_at: "",
updated_at: "",
expanded: false,
};
let isFirstPartialize = true;
export const questionStore = create<QuestionStore>()(
@ -178,13 +63,44 @@ export const questionStore = create<QuestionStore>()(
return state;
},
merge: (persistedState, currentState) => {
const state = persistedState as QuestionStore;
// replace blob urls with ""
Object.values(state.listQuestions).forEach(questions => {
questions.forEach(question => {
if (question.type === "page" && question.content.picture.startsWith("blob:")) {
question.content.picture = "";
}
if (question.type === "images") {
question.content.variants.forEach(variant => {
if (variant.extendedText.startsWith("blob:")) {
variant.extendedText = "";
}
});
}
if (question.type === "varimg") {
question.content.variants.forEach(variant => {
if (variant.extendedText.startsWith("blob:")) {
variant.extendedText = "";
}
});
}
});
});
return {
...currentState,
...state,
};
},
}
)
);
export const updateQuestionsList = (
export const updateQuestionsList = <T = AnyQuizQuestion>(
quizId: number,
index: number,
data: Partial<Question>
data: Partial<T>
) => {
const questionListClone = { ...questionStore.getState()["listQuestions"] };
questionListClone[quizId][index] = {
@ -196,7 +112,7 @@ export const updateQuestionsList = (
export const updateQuestionsListDragAndDrop = (
quizId: number,
updatedQuestions: Question[]
updatedQuestions: AnyQuizQuestion[]
) => {
const questionListClone = { ...questionStore.getState()["listQuestions"] };
questionStore.setState({
@ -207,15 +123,38 @@ export const updateQuestionsListDragAndDrop = (
export const updateVariants = (
quizId: number,
index: number,
variants: Variants[]
variants: QuestionVariant[]
) => {
const listQuestions = { ...questionStore.getState()["listQuestions"] };
listQuestions[quizId][index].content.variants = variants;
questionStore.setState({ listQuestions });
switch (listQuestions[quizId][index].type) {
case "emoji":
const emojiState = listQuestions[quizId][index] as QuizQuestionEmoji;
emojiState.content.variants = variants;
return questionStore.setState({ listQuestions });
case "images":
const imagesState = listQuestions[quizId][index] as QuizQuestionImages;
imagesState.content.variants = variants;
return questionStore.setState({ listQuestions });
case "variant":
const variantState = listQuestions[quizId][index] as QuizQuestionVariant;
variantState.content.variants = variants;
return questionStore.setState({ listQuestions });
case "varimg":
const varImgState = listQuestions[quizId][index] as QuizQuestionVarImg;
varImgState.content.variants = variants;
return questionStore.setState({ listQuestions });
}
};
export const createQuestion = (quizId: number, placeIndex = -1) => {
export const createQuestion = (
quizId: number,
questionType: QuizQuestionType = "nonselected",
placeIndex = -1
) => {
const id = getRandom(1000000, 10000000);
const newData = { ...questionStore.getState()["listQuestions"] };
@ -223,13 +162,30 @@ export const createQuestion = (quizId: number, placeIndex = -1) => {
newData[quizId] = [];
}
newData[quizId].splice(
placeIndex < 0 ? newData[quizId].length : placeIndex,
0,
{ ...DEFAULT_QUESTION, id }
);
const defaultObject = [
QUIZ_QUESTION_BASE,
QUIZ_QUESTION_DATE,
QUIZ_QUESTION_EMOJI,
QUIZ_QUESTION_FILE,
QUIZ_QUESTION_IMAGES,
QUIZ_QUESTION_NUMBER,
QUIZ_QUESTION_PAGE,
QUIZ_QUESTION_RATING,
QUIZ_QUESTION_SELECT,
QUIZ_QUESTION_TEXT,
QUIZ_QUESTION_VARIANT,
QUIZ_QUESTION_VARIMG,
].find((defaultObjectItem) => defaultObjectItem.type === questionType);
questionStore.setState({ listQuestions: newData });
if (defaultObject) {
newData[quizId].splice(
placeIndex < 0 ? newData[quizId].length : placeIndex,
0,
{ ...defaultObject, id }
);
questionStore.setState({ listQuestions: newData });
}
};
export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => {
@ -267,7 +223,7 @@ export const findQuestionById = (quizId: number) => {
let found = null;
questionStore
.getState()
["listQuestions"][quizId].some((quiz: Question, index: number) => {
["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => {
if (quiz.id === quizId) {
found = { quiz, index };
return true;

37
src/stores/quizPreview.ts Normal file

@ -0,0 +1,37 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface QuizPreviewStore {
isPreviewShown: boolean;
currentQuestionIndex: number;
}
export const useQuizPreviewStore = create<QuizPreviewStore>()(
devtools(
(set, get) => ({
isPreviewShown: true,
currentQuestionIndex: 0,
}),
{
name: "quizPreview",
enabled: process.env.NODE_ENV !== "production",
}
)
);
export const showQuizPreview = () => useQuizPreviewStore.setState({ isPreviewShown: true });
export const hideQuizPreview = () => useQuizPreviewStore.setState({ isPreviewShown: false });
export const toggleQuizPreview = () => useQuizPreviewStore.setState(
state => ({ isPreviewShown: !state.isPreviewShown })
);
export const incrementCurrentQuestionIndex = (maxStep: number) => useQuizPreviewStore.setState(
state => ({ currentQuestionIndex: Math.min(state.currentQuestionIndex + 1, maxStep) })
);
export const decrementCurrentQuestionIndex = () => useQuizPreviewStore.setState(
state => ({ currentQuestionIndex: Math.max(state.currentQuestionIndex - 1, 0) })
);

@ -36,6 +36,7 @@ export interface Quizes {
group_id: number,
step: number,
startpage: string,
createResult: boolean,
config: {
type: string,
logo: string,
@ -118,6 +119,7 @@ export const quizStore = create<QuizStore>()(
"group_id": 0,
"step": 1,
"startpage": "",
"createResult": false,
"config": {
"noStartPage": false,
"type": "", // quiz или form

@ -0,0 +1,61 @@
import { Slider } from "@mui/material";
type CustomSliderProps = {
defaultValue?: number;
value?: number | number[];
min?: number;
max?: number;
step?: number;
onChange?: (value: number | number[]) => void;
};
export const CustomSlider = ({
defaultValue,
value,
min = 0,
max = 100,
step,
onChange,
}: CustomSliderProps) => {
const handleChange = ({ type }: Event, newValue: number | number[]) => {
// Для корректной работы слайдера в FireFox
if (type !== "change") {
onChange?.(newValue);
}
};
return (
<Slider
value={value}
defaultValue={defaultValue}
min={min}
max={max}
step={step}
onChange={handleChange}
valueLabelDisplay="auto"
sx={{
color: "#7E2AEA",
height: "12px",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid #9A9AAF`,
},
"& .MuiSlider-thumb": {
height: 32,
width: 32,
border: `6px solid "#7E2AEA`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
},
},
}}
/>
);
};

@ -7,6 +7,7 @@ import {
} from "@mui/material";
import type { ChangeEvent, KeyboardEvent, FocusEvent } from "react";
import type { InputProps } from "@mui/material";
interface CustomTextFieldProps {
placeholder: string;
@ -17,6 +18,7 @@ interface CustomTextFieldProps {
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
text?: string;
sx?: SxProps<Theme>;
InputProps?: Partial<InputProps>;
}
export default function CustomTextField({
@ -28,6 +30,7 @@ export default function CustomTextField({
onChange,
onKeyDown,
onBlur,
InputProps,
}: CustomTextFieldProps) {
const theme = useTheme();
@ -55,6 +58,7 @@ export default function CustomTextField({
},
...sx,
}}
InputProps={InputProps}
inputProps={{
sx: {
borderRadius: "10px",

@ -1,7 +1,7 @@
import { useState } from "react";
import { Box, IconButton, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
import CrossedEyeIcon from "@icons/CrossedEyeIcon";
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
import CopyIcon from "@icons/CopyIcon";
import TrashIcon from "@icons/TrashIcon";
import CountIcon from "@icons/CountIcon";

@ -0,0 +1,68 @@
import CalendarIcon from "@icons/CalendarIcon";
import { Typography, Box, SxProps, Theme, useMediaQuery, useTheme } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import { Dayjs } from "dayjs";
interface Props {
label?: string;
sx?: SxProps<Theme>;
value?: Dayjs | null;
onChange?: (value: Dayjs | null) => void;
}
export default function LabeledDatePicker({ label, value, onChange, sx }: Props) {
const theme = useTheme();
const upLg = useMediaQuery(theme.breakpoints.up("md"));
return (
<Box
sx={{
...sx,
}}
>
{label && (
<Typography
sx={{
fontWeight: 500,
fontSize: "16px",
lineHeight: "20px",
color: "#4D4D4D",
mb: "10px",
}}
>
{label}
</Typography>
)}
<DatePicker
value={value}
onChange={onChange}
slots={{
openPickerIcon: () => <CalendarIcon />,
}}
slotProps={{
openPickerButton: {
sx: {
p: 0,
},
},
}}
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
borderRadius: "10px",
pr: "22px",
"& input": {
py: "11px",
pl: upLg ? "20px" : "13px",
lineHeight: "19px",
},
"& fieldset": {
borderColor: "#9A9AAF",
},
},
}}
/>
</Box>
);
}

@ -1,13 +1,13 @@
import React, { useState, useRef, useEffect, FC } from "react";
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
import {
Box,
Button,
Modal,
Slider,
Typography,
useMediaQuery,
useTheme,
Box,
Button,
Modal,
Slider,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { canvasPreview } from "./utils/canvasPreview";
@ -19,334 +19,336 @@ import "react-image-crop/dist/ReactCrop.css";
import { CropIcon } from "@icons/CropIcon";
interface Iprops {
opened: boolean;
onClose: React.Dispatch<React.SetStateAction<boolean>>;
picture?: string;
opened: boolean;
onClose: React.Dispatch<React.SetStateAction<boolean>>;
picture?: string;
onCropPress?: (imageUrl: string) => void;
}
export const CropModal: FC<Iprops> = ({ opened, onClose, picture }) => {
const [imgSrc, setImgSrc] = useState("");
export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress }) => {
const [imgSrc, setImgSrc] = useState("");
const imgRef = useRef<HTMLImageElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
const hiddenAnchorRef = useRef<HTMLAnchorElement>(null);
const blobUrlRef = useRef("");
const [crop, setCrop] = useState<Crop>();
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
const [rotate, setRotate] = useState(0);
const [darken, setDarken] = useState(0);
const imgRef = useRef<HTMLImageElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
const hiddenAnchorRef = useRef<HTMLAnchorElement>(null);
const blobUrlRef = useRef("");
const [crop, setCrop] = useState<Crop>();
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
const [rotate, setRotate] = useState(0);
const [darken, setDarken] = useState(0);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(786));
useEffect(() => {
if (picture) {
setImgSrc(picture);
}
}, [picture]);
useEffect(() => {
if (picture) {
setImgSrc(picture);
}
}, [picture]);
const styleModal = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: isMobile ? "343px" : "620px",
bgcolor: "background.paper",
boxShadow: 24,
padding: "20px",
borderRadius: "8px",
};
const styleModal = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: isMobile ? "343px" : "620px",
bgcolor: "background.paper",
boxShadow: 24,
padding: "20px",
borderRadius: "8px",
};
const styleSlider = {
width: isMobile ? "350px" : "250px",
color: "#7E2AEA",
height: "12px",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid #9A9AAF`,
},
"& .MuiSlider-thumb": {
height: 26,
width: 26,
border: `6px solid #7E2AEA`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
const styleSlider = {
width: isMobile ? "350px" : "250px",
color: "#7E2AEA",
height: "12px",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid #9A9AAF`,
},
"& .MuiSlider-thumb": {
height: 26,
width: 26,
border: `6px solid #7E2AEA`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
},
},
};
},
},
};
const rotateImage = () => {
const newRotation = (rotate + 90) % 360;
setRotate(newRotation);
};
const rotateImage = () => {
const newRotation = (rotate + 90) % 360;
setRotate(newRotation);
};
const onSelectFile = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setCrop(undefined);
const reader = new FileReader();
reader.addEventListener("load", () =>
setImgSrc(reader.result?.toString() || "")
);
reader.readAsDataURL(event.target.files[0]);
}
};
const onSelectFile = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setCrop(undefined);
const reader = new FileReader();
reader.addEventListener("load", () =>
setImgSrc(reader.result?.toString() || "")
);
reader.readAsDataURL(event.target.files[0]);
}
};
const onDownloadCropClick = () => {
if (!previewCanvasRef.current) {
throw new Error("Crop canvas does not exist");
}
const onDownloadCropClick = () => {
if (!previewCanvasRef.current) {
throw new Error("Crop canvas does not exist");
}
const canvasCopy = document.createElement("canvas");
const ctx = canvasCopy.getContext("2d");
canvasCopy.width = previewCanvasRef.current.width;
canvasCopy.height = previewCanvasRef.current.height;
ctx!.filter = `brightness(${100 - darken}%)`;
ctx!.drawImage(previewCanvasRef.current, 0, 0);
const canvasCopy = document.createElement("canvas");
const ctx = canvasCopy.getContext("2d");
canvasCopy.width = previewCanvasRef.current.width;
canvasCopy.height = previewCanvasRef.current.height;
ctx!.filter = `brightness(${100 - darken}%)`;
ctx!.drawImage(previewCanvasRef.current, 0, 0);
canvasCopy.toBlob((blob) => {
if (!blob) {
throw new Error("Failed to create blob");
}
if (blobUrlRef.current) {
URL.revokeObjectURL(blobUrlRef.current);
}
blobUrlRef.current = URL.createObjectURL(blob);
hiddenAnchorRef.current!.href = blobUrlRef.current;
hiddenAnchorRef.current!.click();
canvasCopy.toBlob((blob) => {
if (!blob) {
throw new Error("Failed to create blob");
}
if (blobUrlRef.current) {
URL.revokeObjectURL(blobUrlRef.current);
}
blobUrlRef.current = URL.createObjectURL(blob);
hiddenAnchorRef.current!.href = blobUrlRef.current;
hiddenAnchorRef.current!.click();
setImgSrc(blobUrlRef.current);
});
};
setImgSrc(blobUrlRef.current);
onCropPress?.(blobUrlRef.current);
});
};
useDebounceEffect(
async () => {
if (
completedCrop?.width &&
completedCrop?.height &&
imgRef.current &&
previewCanvasRef.current
) {
canvasPreview(
imgRef.current,
previewCanvasRef.current,
completedCrop,
rotate
);
}
},
100,
[completedCrop, rotate]
);
useDebounceEffect(
async () => {
if (
completedCrop?.width &&
completedCrop?.height &&
imgRef.current &&
previewCanvasRef.current
) {
canvasPreview(
imgRef.current,
previewCanvasRef.current,
completedCrop,
rotate
);
}
},
100,
[completedCrop, rotate]
);
const [width, setWidth] = useState<number>(0);
const [width, setWidth] = useState<number>(0);
const getImageSize = () => {
if (imgRef.current) {
const imageWidth = imgRef.current.naturalWidth;
const imageHeight = imgRef.current.naturalHeight;
const getImageSize = () => {
if (imgRef.current) {
const imageWidth = imgRef.current.naturalWidth;
const imageHeight = imgRef.current.naturalHeight;
const aspect = imageWidth / imageHeight;
const aspect = imageWidth / imageHeight;
if (aspect <= 1.333) {
setWidth(240);
}
if (aspect >= 1.5) {
setWidth(580);
}
if (aspect >= 1.778) {
setWidth(580);
}
}
};
if (aspect <= 1.333) {
setWidth(240);
}
if (aspect >= 1.5) {
setWidth(580);
}
if (aspect >= 1.778) {
setWidth(580);
}
}
};
return (
<>
<Modal
open={opened}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={styleModal}>
<Box
sx={{
height: "320px",
padding: "10px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
return (
<>
<Modal
open={opened}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={styleModal}>
<Box
sx={{
height: "320px",
padding: "10px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{imgSrc && (
<ReactCrop
crop={crop}
onChange={(_, percentCrop) => setCrop(percentCrop)}
onComplete={(c) => setCompletedCrop(c)}
maxWidth={500}
minWidth={50}
maxHeight={320}
minHeight={50}
>
<img
onLoad={getImageSize}
ref={imgRef}
alt="Crop me"
src={imgSrc}
style={{
filter: `brightness(${100 - darken}%)`,
transform: ` rotate(${rotate}deg)`,
maxWidth: "580px",
maxHeight: "320px",
}}
width={width}
/>
</ReactCrop>
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{imgSrc && (
<ReactCrop
crop={crop}
onChange={(_, percentCrop) => setCrop(percentCrop)}
onComplete={(c) => setCompletedCrop(c)}
maxWidth={500}
minWidth={50}
maxHeight={320}
minHeight={50}
>
<img
onLoad={getImageSize}
ref={imgRef}
alt="Crop me"
src={imgSrc}
style={{
filter: `brightness(${100 - darken}%)`,
transform: ` rotate(${rotate}deg)`,
maxWidth: "580px",
maxHeight: "320px",
}}
width={width}
/>
</ReactCrop>
)}
</Box>
<Box
sx={{
color: "#7E2AEA",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "16xp",
fontWeight: "600",
marginBottom: "50px",
}}
>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.width ? Math.round(crop.width) + "px" : ""}
</Typography>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.height ? Math.round(crop.height) + "px" : ""}
</Typography>
</Box>
<Box
sx={{
display: isMobile ? "block" : "flex",
alignItems: "end",
justifyContent: "space-between",
}}
>
<ResetIcon
onClick={rotateImage}
style={{ marginBottom: "10px", cursor: "pointer" }}
/>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Размер
</Typography>
<Slider
sx={styleSlider}
value={width}
min={50}
max={580}
step={1}
onChange={(_, newValue) => {
setWidth(newValue as number);
}}
/>
</Box>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Затемнение
</Typography>
<Slider
sx={styleSlider}
value={darken}
min={0}
max={100}
step={1}
onChange={(_, newValue) => setDarken(newValue as number)}
/>
</Box>
</Box>
<Box
sx={{
marginTop: "40px",
width: "100%",
display: "flex",
justifyContent: "end",
}}
>
<input
style={{ display: "none", zIndex: "-999" }}
ref={fileInputRef}
type="file"
accept="image/*"
onChange={onSelectFile}
/>
<Button
onClick={() => fileInputRef.current?.click()}
disableRipple
sx={{
width: "215px",
height: "48px",
color: "#7E2AEA",
borderRadius: "8px",
border: "1px solid #7E2AEA",
marginRight: "10px",
}}
>
Загрузить оригинал
</Button>
<Button
onClick={onDownloadCropClick}
disableRipple
variant="contained"
sx={{
padding: "10px 20px",
borderRadius: "8px",
background: theme.palette.brightPurple.main,
fontSize: "18px",
}}
>
<CropIcon />
Обрезать
</Button>
</Box>
</Box>
</Modal>
{completedCrop && (
<div>
<canvas
ref={previewCanvasRef}
style={{
display: "none",
zIndex: "-999",
border: "1px solid black",
objectFit: "contain",
width: completedCrop.width,
height: completedCrop.height,
}}
/>
</div>
)}
</Box>
<Box
sx={{
color: "#7E2AEA",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "16xp",
fontWeight: "600",
marginBottom: "50px",
}}
>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.width ? Math.round(crop.width) + "px" : ""}
</Typography>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.height ? Math.round(crop.height) + "px" : ""}
</Typography>
</Box>
<Box
sx={{
display: isMobile ? "block" : "flex",
alignItems: "end",
justifyContent: "space-between",
}}
>
<ResetIcon
onClick={rotateImage}
style={{ marginBottom: "10px", cursor: "pointer" }}
/>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Размер
</Typography>
<Slider
sx={styleSlider}
value={width}
min={50}
max={580}
step={1}
onChange={(_, newValue) => {
setWidth(newValue as number);
}}
/>
</Box>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Затемнение
</Typography>
<Slider
sx={styleSlider}
value={darken}
min={0}
max={100}
step={1}
onChange={(_, newValue) => setDarken(newValue as number)}
/>
</Box>
</Box>
<Box
sx={{
marginTop: "40px",
width: "100%",
display: "flex",
justifyContent: "end",
}}
>
<input
style={{ display: "none", zIndex: "-999" }}
ref={fileInputRef}
type="file"
accept="image/*"
onChange={onSelectFile}
/>
<Button
onClick={() => fileInputRef.current?.click()}
disableRipple
sx={{
width: "215px",
height: "48px",
color: "#7E2AEA",
borderRadius: "8px",
border: "1px solid #7E2AEA",
marginRight: "10px",
}}
>
Загрузить оригинал
</Button>
<Button
onClick={onDownloadCropClick}
disableRipple
variant="contained"
sx={{
padding: "10px 20px",
borderRadius: "8px",
background: theme.palette.brightPurple.main,
fontSize: "18px",
}}
>
<CropIcon />
Обрезать
</Button>
</Box>
</Box>
</Modal>
{completedCrop && (
<div>
<canvas
ref={previewCanvasRef}
style={{
display: "none",
zIndex: "-999",
border: "1px solid black",
objectFit: "contain",
width: completedCrop.width,
height: completedCrop.height,
}}
/>
</div>
)}
<div>
<a
href="#hidden"
ref={hiddenAnchorRef}
download
style={{
display: "none",
}}
>
Hidden download
</a>
</div>
</>
);
<div>
<a
href="#hidden"
ref={hiddenAnchorRef}
download
style={{
display: "none",
}}
>
Hidden download
</a>
</div>
</>
);
};

@ -0,0 +1,130 @@
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import { Box, IconButton } from "@mui/material";
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
import { useLayoutEffect, useRef } from "react";
import { Rnd } from "react-rnd";
import QuizPreviewLayout from "./QuizPreviewLayout";
import ResizeIcon from "./ResizeIcon";
import VisibilityIcon from '@mui/icons-material/Visibility';
const DRAG_PARENT_MARGIN = 25;
const NAVBAR_HEIGHT = 81;
const DRAG_PARENT_BOTTOM_MARGIN = 65;
interface RndPositionAndSize {
x: number;
y: number;
width: string;
height: string;
}
export default function QuizPreview() {
const isPreviewShown = useQuizPreviewStore(state => state.isPreviewShown);
const rndParentRef = useRef<HTMLDivElement>(null);
const rndRef = useRef<Rnd | null>(null);
const rndPositionAndSizeRef = useRef<RndPositionAndSize>({ x: 0, y: 0, width: "340", height: "480" });
const isFirstShowRef = useRef<boolean>(true);
useLayoutEffect(function stickPreviewToBottomRight() {
const rnd = rndRef.current;
const rndSelfElement = rnd?.getSelfElement();
if (!rnd || !rndSelfElement || !rndParentRef.current || !isFirstShowRef.current) return;
const rndParentRect = rndParentRef.current.getBoundingClientRect();
const rndRect = rndSelfElement.getBoundingClientRect();
const x = rndParentRect.width - rndRect.width;
const y = rndParentRect.height - rndRect.height;
rnd.updatePosition({ x, y });
rndPositionAndSizeRef.current.x = x;
rndPositionAndSizeRef.current.y = y;
isFirstShowRef.current = false;
}, [isPreviewShown]);
return (
<Box
ref={rndParentRef}
sx={{
position: "fixed",
top: NAVBAR_HEIGHT + DRAG_PARENT_MARGIN,
left: DRAG_PARENT_MARGIN,
bottom: DRAG_PARENT_BOTTOM_MARGIN,
right: DRAG_PARENT_MARGIN,
// backgroundColor: "rgba(0, 100, 0, 0.2)",
pointerEvents: "none",
zIndex: 100,
}}
>
{isPreviewShown &&
<Rnd
minHeight={300}
minWidth={340}
bounds="parent"
ref={rndRef}
dragHandleClassName="quiz-preview-draghandle"
default={{
x: rndPositionAndSizeRef.current.x,
y: rndPositionAndSizeRef.current.y,
width: rndPositionAndSizeRef.current.width,
height: rndPositionAndSizeRef.current.height
}}
onResizeStop={(e, direction, ref, delta, position) => {
rndPositionAndSizeRef.current.x = position.x;
rndPositionAndSizeRef.current.y = position.y;
rndPositionAndSizeRef.current.width = ref.style.width;
rndPositionAndSizeRef.current.height = ref.style.height;
}}
onDragStop={(e, d) => {
rndPositionAndSizeRef.current.x = d.x;
rndPositionAndSizeRef.current.y = d.y;
}}
onDragStart={(e, d) => {
e.preventDefault();
}}
enableResizing={{
topLeft: isPreviewShown,
}}
resizeHandleComponent={{
topLeft: <ResizeIcon />
}}
resizeHandleStyles={{
topLeft: {
top: "-1px",
left: "-1px",
}
}}
style={{
pointerEvents: "auto",
}}
>
<QuizPreviewLayout />
<IconButton
className="quiz-preview-draghandle"
sx={{
position: "absolute",
bottom: -54,
right: 46,
cursor: "move",
}}
>
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
</IconButton>
</Rnd>
}
<IconButton
onClick={toggleQuizPreview}
sx={{
position: "absolute",
right: 0,
bottom: -54,
pointerEvents: "auto",
}}
>
<VisibilityIcon sx={{ height: "30px", width: "30px" }} />
</IconButton>
</Box>
);
}

@ -0,0 +1,133 @@
import { Box, Button, LinearProgress, Paper, Typography } from "@mui/material";
import { questionStore } from "@root/questions";
import { decrementCurrentQuestionIndex, incrementCurrentQuestionIndex, useQuizPreviewStore } from "@root/quizPreview";
import { AnyQuizQuestion, DefiniteQuestionType } from "model/questionTypes/shared";
import { FC, useEffect } from "react";
import { useParams } from "react-router-dom";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import Date from "./QuizPreviewQuestionTypes/Date";
import Emoji from "./QuizPreviewQuestionTypes/Emoji";
import File from "./QuizPreviewQuestionTypes/File";
import Images from "./QuizPreviewQuestionTypes/Images";
import Number from "./QuizPreviewQuestionTypes/Number";
import Page from "./QuizPreviewQuestionTypes/Page";
import Rating from "./QuizPreviewQuestionTypes/Rating";
import Select from "./QuizPreviewQuestionTypes/Select";
import Text from "./QuizPreviewQuestionTypes/Text";
import Variant from "./QuizPreviewQuestionTypes/Variant";
import Varimg from "./QuizPreviewQuestionTypes/Varimg";
const QuestionPreviewComponentByType: Record<DefiniteQuestionType, FC<any>> = {
variant: Variant,
images: Images,
varimg: Varimg,
emoji: Emoji,
text: Text,
select: Select,
date: Date,
number: Number,
file: File,
page: Page,
rating: Rating,
};
export default function QuizPreviewLayout() {
const quizId = useParams().quizId ?? 0;
const listQuestions = questionStore(state => state.listQuestions);
const currentQuizStep = useQuizPreviewStore(state => state.currentQuestionIndex);
const quizQuestions: AnyQuizQuestion[] | undefined = listQuestions[quizId];
const nonDeletedQuizQuestions = quizQuestions?.filter(question => !question.deleted);
const maxCurrentQuizStep = nonDeletedQuizQuestions?.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
const currentProgress = Math.floor((currentQuizStep / maxCurrentQuizStep) * 100);
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
const QuestionComponent = currentQuestion
? QuestionPreviewComponentByType[currentQuestion.type as DefiniteQuestionType]
: null;
const questionElement = QuestionComponent
? <QuestionComponent key={currentQuestion.id} question={currentQuestion} />
: null;
useEffect(function resetCurrentQuizStep() {
if (currentQuizStep > maxCurrentQuizStep) {
decrementCurrentQuestionIndex();
}
}, [currentQuizStep, maxCurrentQuizStep]);
return (
<Paper sx={{
height: "100%",
display: "flex",
flexDirection: "column",
flexGrow: 1,
borderRadius: "12px",
pointerEvents: "auto",
}}>
<Box sx={{
p: "16px",
whiteSpace: "break-spaces",
overflowY: "auto",
flexGrow: 1,
}}>
{questionElement}
</Box>
<Box sx={{
mt: "auto",
p: "16px",
display: "flex",
borderTop: "1px solid #E3E3E3",
alignItems: "center",
}}>
<Box sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography>
{nonDeletedQuizQuestions.length > 0
? `Вопрос ${currentQuizStep + 1} из ${nonDeletedQuizQuestions.length}`
: "Нет вопросов"
}
</Typography>
{nonDeletedQuizQuestions.length > 0 &&
<LinearProgress
variant="determinate"
value={currentProgress}
sx={{
"&.MuiLinearProgress-colorPrimary": {
backgroundColor: "fadePurple.main",
},
"& .MuiLinearProgress-barColorPrimary": {
backgroundColor: "brightPurple.main",
},
}}
/>
}
</Box>
<Box sx={{
ml: 2,
display: "flex",
gap: 1,
}}>
<Button
variant="outlined"
onClick={decrementCurrentQuestionIndex}
disabled={currentQuizStep === 0}
sx={{ px: 1, minWidth: 0 }}
>
<ArrowLeft />
</Button>
<Button
variant="contained"
onClick={() => incrementCurrentQuestionIndex(maxCurrentQuizStep)}
disabled={currentQuizStep >= maxCurrentQuizStep}
>Далее</Button>
</Box>
</Box>
</Paper>
);
}

@ -0,0 +1,22 @@
import { Box, Typography } from "@mui/material";
import LabeledDatePicker from "@ui_kit/LabeledDatePicker";
import { QuizQuestionDate } from "model/questionTypes/date";
interface Props {
question: QuizQuestionDate;
}
export default function Date({ question }: Props) {
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<LabeledDatePicker />
</Box>
);
}

@ -0,0 +1,39 @@
import InfoIcon from "@icons/InfoIcon";
import { Box, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Tooltip, Typography } from "@mui/material";
import { QuizQuestionEmoji } from "model/questionTypes/emoji";
import { useState, ChangeEvent } from "react";
interface Props {
question: QuizQuestionEmoji;
}
export default function Emoji({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl fullWidth>
<FormLabel id="quiz-question-radio-group">{question.title}</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
>
{question.content.variants.map((variant, index) => (
<FormControlLabel key={index} value={variant.answer} control={<Radio />} label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography>{`${variant.extendedText} ${variant.answer}`}</Typography>
<Tooltip title={variant.hints} placement="right">
<Box><InfoIcon /></Box>
</Tooltip>
</Box>
} />
))}
</RadioGroup>
</FormControl>
);
}

@ -0,0 +1,44 @@
import { Box, Button, Typography } from "@mui/material";
import { QuizQuestionFile } from "model/questionTypes/file";
import { ChangeEvent, useRef, useState } from "react";
interface Props {
question: QuizQuestionFile;
}
export default function File({ question }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
if (!event.target.files?.[0]) return setFile(null);
setFile(event.target.files[0]);
}
return (
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Button
variant="contained"
onClick={() => fileInputRef.current?.click()}
>
Загрузить файл
<input
ref={fileInputRef}
onChange={handleFileChange}
type="file"
style={{
display: "none",
}}
/>
</Button>
{file && <Typography>Выбран файл: {file.name}</Typography>}
</Box>
);
}

@ -0,0 +1,90 @@
import InfoIcon from "@icons/InfoIcon";
import { Box, ButtonBase, Divider, Tooltip, Typography, useTheme } from "@mui/material";
import { QuizQuestionImages } from "model/questionTypes/images";
import { useEffect, useState } from "react";
interface Props {
question: QuizQuestionImages;
}
export default function Images({ question }: Props) {
const theme = useTheme();
const [selectedVariants, setSelectedVariants] = useState<number[]>([]);
function handleVariantClick(index: number) {
if (!question.content.multi) return setSelectedVariants([index]);
const newSelectedVariants = [...selectedVariants];
if (newSelectedVariants.includes(index)) {
newSelectedVariants.splice(newSelectedVariants.indexOf(index), 1);
} else {
newSelectedVariants.push(index);
}
setSelectedVariants(newSelectedVariants);
}
useEffect(function resetSelectedVariants() {
setSelectedVariants([]);
}, [question.content.multi]);
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Box sx={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))",
gap: 1,
}}>
{question.content.variants.map((variant, index) => (
<ButtonBase
key={index}
onClick={() => handleVariantClick(index)}
sx={{
display: "flex",
flexDirection: "column",
borderRadius: "8px",
overflow: "hidden",
border: "1px solid",
borderColor: selectedVariants.includes(index) ? theme.palette.brightPurple.main : "#E3E3E3",
}}
>
{variant.extendedText ?
<img
src={variant.extendedText}
alt="question variant"
style={{
width: "100%",
display: "block",
objectFit: "scale-down",
flexGrow: 1,
}}
/>
:
<Typography p={2}>Картинка отсутствует</Typography>
}
<Divider sx={{ width: "100%" }} />
<Box sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 2,
p: 1,
}}>
<Typography>{variant.answer}</Typography>
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
</ButtonBase>
))}
</Box>
</Box>
);
}

@ -0,0 +1,47 @@
import { Box, Typography } from "@mui/material";
import { CustomSlider } from "@ui_kit/CustomSlider";
import { QuizQuestionNumber } from "model/questionTypes/number";
import { useLayoutEffect, useState } from "react";
interface Props {
question: QuizQuestionNumber;
}
export default function Number({ question }: Props) {
const [sliderValues, setSliderValues] = useState<number | number[]>(0);
const start = question.content.start;
const min = parseInt(question.content.range.split("—")[0]);
const max = parseInt(question.content.range.split("—")[1]);
useLayoutEffect(() => {
if (question.content.chooseRange) {
setSliderValues([start, start + (max - start) / 2]);
} else {
setSliderValues(start);
}
}, [max, question.content.chooseRange, start]);
return (
<Box sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Box sx={{
px: 2,
}}>
<CustomSlider
value={sliderValues}
onChange={value => setSliderValues(value)}
min={min}
max={max}
defaultValue={start}
step={question.content.step}
/>
</Box>
</Box>
);
}

@ -0,0 +1,34 @@
import { Box, Typography } from "@mui/material";
import { QuizQuestionPage } from "model/questionTypes/page";
interface Props {
question: QuizQuestionPage;
}
export default function Page({ question }: Props) {
return (
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: 1,
}}>
<Typography variant="h6">{question.title}</Typography>
<Typography>{question.content.text}</Typography>
{question.content.picture &&
<img
src={question.content.picture}
alt=""
style={{
width: "100%",
display: "block",
objectFit: "scale-down",
flexGrow: 1,
}}
/>
}
</Box>
);
}

Some files were not shown because too many files have changed in this diff Show More