Compare commits
96 Commits
e331f69885
...
feat/filte
| Author | SHA1 | Date | |
|---|---|---|---|
| a63a229b64 | |||
| 775f24cffa | |||
|
|
78b72b0edc | ||
|
|
333fe79c8b | ||
|
|
9d10c8501a | ||
|
|
d64ece382a | ||
|
|
f91f821f86 | ||
|
|
b5301f948a | ||
|
|
dd589790c2 | ||
|
|
1fcc5ed70d | ||
|
|
41dbe81001 | ||
|
|
7b685ad99e | ||
| 2f1e1dc040 | |||
|
|
70e8a6877c | ||
|
|
87fd3121f9 | ||
|
|
4f9434163e | ||
|
|
350d452a7b | ||
| 9a0669df13 | |||
|
|
c0883fc2bc | ||
| 566bce4663 | |||
| c828718498 | |||
|
|
69c280b266 | ||
| 6794b01ac8 | |||
| 1cb586f55a | |||
|
|
df21879c0d | ||
|
|
30c9c86c93 | ||
|
|
2925d0f17b | ||
|
|
752dabd015 | ||
|
|
815f11d5bc | ||
| 02eb0e60b7 | |||
| a64ac93935 | |||
| 66a48d1c7e | |||
| 26c66f16b4 | |||
| 02e50bb2f9 | |||
| fadc62c8f0 | |||
| 4759f6f7ee | |||
| 14f2164a82 | |||
| 14ef1f9bad | |||
| dc99318ff0 | |||
| d2fc5f4d5c | |||
| 938bd48fff | |||
| 96f819dc91 | |||
| 25eee8adf5 | |||
| d2b2a29d3d | |||
| 1cf71261d1 | |||
| 88552eb04f | |||
| ab92c99321 | |||
| 02963de893 | |||
| 48550416d9 | |||
| 878c5ffd68 | |||
|
|
6e37fe93f7 | ||
|
|
72a2667549 | ||
|
|
39db7b4d26 | ||
| ff25c0ecb9 | |||
|
|
f1a93bffb5 | ||
| aa231d4f43 | |||
|
|
f254d57db4 | ||
| 106f835934 | |||
| f9b30a4cfd | |||
| 5e4a99529d | |||
| 4d585002d7 | |||
| b073fe3fdf | |||
| 312cc229d8 | |||
| 11b1d670d0 | |||
|
|
522ea36bb9 | ||
|
|
8be391c8e1 | ||
|
|
ea80304c21 | ||
| 771f75ef08 | |||
|
|
edf9b2c82b | ||
|
|
8c3bf8a8ed | ||
| a88d3657bf | |||
|
|
1656ce8690 | ||
|
|
7cdbec53ee | ||
| eee00f0797 | |||
|
|
33845b743d | ||
|
|
059139e213 | ||
|
|
005e7a0ac9 | ||
| 9ee59256a9 | |||
| c2784dcf45 | |||
| 64ed9b8eda | |||
| e9814f36bf | |||
| 0bd883df59 | |||
| 1657b0c5e9 | |||
| a9673b260f | |||
|
|
8ee5ca5528 | ||
| e1e335098e | |||
| 619975d1e0 | |||
| f29bc83d56 | |||
| 61347d1aee | |||
| ac7a99ef15 | |||
| eb3f1f7e3f | |||
| 3e02ea5843 | |||
| d06aeeb246 | |||
|
|
801f9ac1e3 | ||
|
|
cbbb376fd6 | ||
|
|
faaec7c718 |
165
package-lock.json
generated
165
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "multi-stub",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "multi-stub",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ai": "^4.1.13",
|
||||
@@ -25,7 +25,9 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongodb": "^6.12.0",
|
||||
"mongoose": "^8.9.2",
|
||||
"mongoose-sequence": "^6.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pbkdf2-password": "^1.2.1",
|
||||
"rotating-file-stream": "^3.2.5",
|
||||
"socket.io": "^4.8.1",
|
||||
@@ -2083,6 +2085,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
@@ -2133,6 +2141,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -2436,9 +2450,19 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -2701,6 +2725,21 @@
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"engines": [
|
||||
"node >= 0.8"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^2.2.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
@@ -2767,6 +2806,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
@@ -4571,6 +4616,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -5642,6 +5693,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -5828,6 +5885,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
@@ -5995,6 +6061,19 @@
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose-sequence": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose-sequence/-/mongoose-sequence-6.0.1.tgz",
|
||||
"integrity": "sha512-uXnLCW9pu2V49Xw8BmdXdeRugd2mv+ntu3nT2Bbm33pNRmmvHE2GKA+8BASKoQt960McLX4VL78wkb492f6MoQ==",
|
||||
"license": "GPL-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.5",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mongoose": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/mongodb": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
|
||||
@@ -6124,6 +6203,36 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "1.4.5-lts.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
||||
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.0.0",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@@ -6681,6 +6790,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -6808,6 +6923,27 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -7388,6 +7524,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
@@ -7780,6 +7924,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
@@ -8083,6 +8233,15 @@
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-stub",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -39,7 +39,9 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongodb": "^6.12.0",
|
||||
"mongoose": "^8.9.2",
|
||||
"mongoose-sequence": "^6.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pbkdf2-password": "^1.2.1",
|
||||
"rotating-file-stream": "^3.2.5",
|
||||
"socket.io": "^4.8.1",
|
||||
|
||||
@@ -89,6 +89,8 @@ app.use("/freetracker", require("./routers/freetracker"))
|
||||
app.use("/dhs-testing", require("./routers/dhs-testing"))
|
||||
app.use("/gamehub", require("./routers/gamehub"))
|
||||
app.use("/esc", require("./routers/esc"))
|
||||
app.use('/connectme', require('./routers/connectme'))
|
||||
app.use('/questioneer', require('./routers/questioneer'))
|
||||
|
||||
app.use(require("./error"))
|
||||
|
||||
|
||||
60
server/models/questionnaire.js
Normal file
60
server/models/questionnaire.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// Типы вопросов
|
||||
const QUESTION_TYPES = {
|
||||
SINGLE_CHOICE: 'single_choice', // Один вариант
|
||||
MULTIPLE_CHOICE: 'multiple_choice', // Несколько вариантов
|
||||
TEXT: 'text', // Текстовый ответ
|
||||
RATING: 'rating', // Оценка по шкале
|
||||
TAG_CLOUD: 'tag_cloud' // Облако тегов
|
||||
};
|
||||
|
||||
// Типы отображения
|
||||
const DISPLAY_TYPES = {
|
||||
DEFAULT: 'default',
|
||||
TAG_CLOUD: 'tag_cloud',
|
||||
VOTING: 'voting',
|
||||
POLL: 'poll'
|
||||
};
|
||||
|
||||
// Схема варианта ответа
|
||||
const optionSchema = new mongoose.Schema({
|
||||
text: { type: String, required: true },
|
||||
count: { type: Number, default: 0 } // счетчик голосов
|
||||
});
|
||||
|
||||
// Схема вопроса
|
||||
const questionSchema = new mongoose.Schema({
|
||||
text: { type: String, required: true },
|
||||
type: {
|
||||
type: String,
|
||||
enum: Object.values(QUESTION_TYPES),
|
||||
required: true
|
||||
},
|
||||
options: [optionSchema],
|
||||
required: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
// Схема опроса
|
||||
const questionnaireSchema = new mongoose.Schema({
|
||||
title: { type: String, required: true },
|
||||
description: { type: String },
|
||||
questions: [questionSchema],
|
||||
displayType: {
|
||||
type: String,
|
||||
enum: Object.values(DISPLAY_TYPES),
|
||||
default: DISPLAY_TYPES.DEFAULT
|
||||
},
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
adminLink: { type: String, required: true }, // ссылка для редактирования
|
||||
publicLink: { type: String, required: true } // ссылка для голосования
|
||||
});
|
||||
|
||||
const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema);
|
||||
|
||||
module.exports = {
|
||||
Questionnaire,
|
||||
QUESTION_TYPES,
|
||||
DISPLAY_TYPES
|
||||
};
|
||||
8
server/routers/connectme/index.js
Normal file
8
server/routers/connectme/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const { Router } = require('express')
|
||||
const router = Router()
|
||||
|
||||
router.get('/cities', (request, response) => {
|
||||
response.send(require('./json/cities.json'))
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
85
server/routers/connectme/json/cities.json
Normal file
85
server/routers/connectme/json/cities.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Моска"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Санкт-петербург"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Новосибирска"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Екатеринбург"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Казань"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Нижний новгород"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "Челябинск"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "Самара"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "Омск"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "Ростов-на-дону"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title": "Уфа"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "Красноярск"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"title": "Пермь"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"title": "Воронеж"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "Волгоград"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"title": "Краснодар"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"title": "Тюмень"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"title": "Ижевск"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"title": "Барнаул"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"title": "Владивосток"
|
||||
}
|
||||
],
|
||||
"count": 20
|
||||
}
|
||||
2
server/routers/dogsitters-finder/const.js
Normal file
2
server/routers/dogsitters-finder/const.js
Normal file
@@ -0,0 +1,2 @@
|
||||
exports.DSF_AUTH_USER_MODEL_NAME = 'DSF_AUTH_USER'
|
||||
exports.DSF_INTERACTION_MODEL_NAME = 'DSF_INTERACTION'
|
||||
@@ -7,29 +7,29 @@ router.get("/users", (request, response) => {
|
||||
router.post("/auth", (request, response) => {
|
||||
const { phoneNumber, password } = request.body;
|
||||
console.log(phoneNumber, password);
|
||||
if (phoneNumber === "89999999999") {
|
||||
response.send(require("./json/auth/dogsitter.success.json"));
|
||||
} else if (phoneNumber === "89555555555") {
|
||||
response.status(400).send(require("./json/auth/error.json"));
|
||||
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
|
||||
response.send(require("./json/auth/success.json"));
|
||||
} else {
|
||||
response.send(require("./json/auth/owner.success.json"));
|
||||
response.status(401).send(require("./json/auth/error.json"));
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/auth/2fa", (request, response) => {
|
||||
const { code } = request.body;
|
||||
if (code === "0000") {
|
||||
response.send(require("./json/2fa/success.json"));
|
||||
const { phoneNumber, code } = request.body;
|
||||
if (code === "0000" && phoneNumber === "89999999999") {
|
||||
response.send(require("./json/2fa/dogsitter.success.json"));
|
||||
} else if (code === "0000" && phoneNumber === "89559999999") {
|
||||
response.send(require("./json/2fa/owner.success.json"));
|
||||
} else {
|
||||
response.status(400).send(require("./json/2fa/error.json"));
|
||||
response.status(401).send(require("./json/2fa/error.json"));
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/register", (request, response) => {
|
||||
const { firstName, secondName, phoneNumber, password, role } = request.body;
|
||||
console.log(phoneNumber, password, role);
|
||||
if (phoneNumber === "89283244141" || phoneNumber === "89872855893") {
|
||||
response.status(400).send(require("./json/register/error.json"));
|
||||
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
|
||||
response.status(401).send(require("./json/register/error.json"));
|
||||
} else if (role === "dogsitter") {
|
||||
response.send(require("./json/register/dogsitter.success.json"));
|
||||
} else {
|
||||
@@ -37,4 +37,192 @@ router.post("/register", (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
router.get("/auth/session", (request, response) => {
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
return response.status(401).json({ error: "Authorization header missing" });
|
||||
}
|
||||
|
||||
// Берём сам токен из заголовка
|
||||
const token = authHeader.split(" ")[1];
|
||||
|
||||
if (!token) {
|
||||
return response.status(401).json({ error: "Bearer token missing" });
|
||||
}
|
||||
|
||||
const jwt = require("jsonwebtoken");
|
||||
const secretKey = "secret";
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, secretKey);
|
||||
|
||||
if (decoded.role === "dogsitter") {
|
||||
response.send(require("./json/role/dogsitter.success.json"));
|
||||
} else {
|
||||
response.send(require("./json/role/owner.success.json"));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("token e:", e);
|
||||
return response.status(403).json({ error: "Invalid token" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Проверка взаимодействия между пользователем и догситтером
|
||||
router.get("/interactions/check", (req, res) => {
|
||||
const { owner_id, dogsitter_id } = req.query;
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
|
||||
const interactions = usersFile.interactions || [];
|
||||
|
||||
const exists = interactions.some(
|
||||
(interaction) =>
|
||||
interaction.owner_id === Number(owner_id) &&
|
||||
interaction.dogsitter_id === Number(dogsitter_id)
|
||||
);
|
||||
|
||||
res.json({ exists });
|
||||
});
|
||||
|
||||
// Добавление нового взаимодействия
|
||||
router.post("/interactions", (req, res) => {
|
||||
const { owner_id, dogsitter_id, interaction_type } = req.body;
|
||||
|
||||
if (!owner_id || !dogsitter_id || !interaction_type) {
|
||||
return res.status(400).json({ error: "Missing required fields" });
|
||||
}
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
|
||||
if (!usersFile.interactions) {
|
||||
usersFile.interactions = [];
|
||||
}
|
||||
|
||||
// Проверяем, существует ли уже такое взаимодействие
|
||||
const exists = usersFile.interactions.some(
|
||||
(interaction) =>
|
||||
interaction.owner_id === Number(owner_id) &&
|
||||
interaction.dogsitter_id === Number(dogsitter_id)
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
usersFile.interactions.push({
|
||||
owner_id: Number(owner_id),
|
||||
dogsitter_id: Number(dogsitter_id),
|
||||
interaction_type,
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
usersFilePath,
|
||||
JSON.stringify(usersFile, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Добавлено взаимодействие: owner_id=${owner_id}, dogsitter_id=${dogsitter_id}`
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.get("/dogsitter-viewing", (req, res) => {
|
||||
const { id } = req.query;
|
||||
console.log(`Получен запрос для dogsitter с ID: ${id}`);
|
||||
|
||||
const usersFile = require("./json/users/users.json");
|
||||
const users = usersFile.data; // Извлекаем массив из свойства "data"
|
||||
|
||||
const user = users.find((user) => user.id === Number(id));
|
||||
|
||||
if (user) {
|
||||
res.json(user); // Возвращаем найденного пользователя
|
||||
} else {
|
||||
res.status(404).json({ error: "User not found" }); // Если пользователь не найден
|
||||
}
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
||||
router.post('/dogsitter-viewing/rating/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { rating } = req.body;
|
||||
|
||||
if (!rating || rating < 1 || rating > 5) {
|
||||
return res.status(400).json({ error: 'Некорректная оценка' });
|
||||
}
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
const users = usersFile.data;
|
||||
|
||||
const userIndex = users.findIndex(user => user.id === Number(id));
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({ error: 'Догситтер не найден' });
|
||||
}
|
||||
|
||||
if (!users[userIndex].ratings) {
|
||||
users[userIndex].ratings = [];
|
||||
}
|
||||
users[userIndex].ratings.push(rating);
|
||||
|
||||
if (users[userIndex].ratings.length > 100) {
|
||||
users[userIndex].ratings.shift();
|
||||
}
|
||||
|
||||
const total = users[userIndex].ratings.reduce((sum, r) => sum + r, 0);
|
||||
users[userIndex].rating = parseFloat((total / users[userIndex].ratings.length).toFixed(2));
|
||||
|
||||
fs.writeFileSync(usersFilePath, JSON.stringify({ data: users }, null, 2), 'utf8');
|
||||
|
||||
console.log(`Обновлен рейтинг догситтера ${id}: ${users[userIndex].rating}`);
|
||||
|
||||
res.json({ rating: users[userIndex].rating, ratings: users[userIndex].ratings });
|
||||
});
|
||||
|
||||
|
||||
router.patch('/users/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
console.log('Полученные данные для обновления:', updateData);
|
||||
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
const users = usersFile.data;
|
||||
|
||||
const userIndex = users.findIndex((user) => user.id === Number(id));
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
users[userIndex] = { ...users[userIndex], ...updateData };
|
||||
|
||||
fs.writeFileSync(
|
||||
usersFilePath,
|
||||
JSON.stringify({ data: users }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
console.log('Обновлённые данные пользователя:', users[userIndex]);
|
||||
|
||||
res.json(users[userIndex]);
|
||||
});
|
||||
|
||||
|
||||
module.exports = router
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.7q66wTNyLZp3TGFYF_JdU-yhlWViJulTxP_PCQzO4OI"
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid code."
|
||||
"message": "Invalid code",
|
||||
"statusCode": 401
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mywicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.sI9839YXveTpEWhdpr5QbCYllt6hHYO7NsrQDcrXZIQ"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Two-factor authentication passed."
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 1,
|
||||
"phoneNumber": 89283244141,
|
||||
"firstName": "Вася",
|
||||
"secondName": "Пупкин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
||||
"price": 1500,
|
||||
"aboutMe": "Я люблю собак"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"error": "Пользователь не найден"
|
||||
}
|
||||
"message": "Неверный логин или пароль",
|
||||
"error": "Unauthorized",
|
||||
"statusCode": 401
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 3,
|
||||
"phoneNumber": 89872855893,
|
||||
"firstName": "Гадий",
|
||||
"secondName": "Петрович",
|
||||
"role": "owner"
|
||||
}
|
||||
}
|
||||
5
server/routers/dogsitters-finder/json/auth/success.json
Normal file
5
server/routers/dogsitters-finder/json/auth/success.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Первый фактор аутентификации пройден",
|
||||
"statusCode": 200
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 5,
|
||||
"phoneNumber": 89555555555,
|
||||
"firstName": "Масяня",
|
||||
"secondName": "Карлова",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
||||
"price": 100,
|
||||
"aboutMe": "Все на свете - собаки"
|
||||
}
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.T9V3-f3rD1deA5a2J-tYNw0cACEpzKHbhMPkc7gh8c0"
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"error": "Пользователь с таким номером телефона уже существует"
|
||||
"message": "Такой пользователь уже был зарегистрирован",
|
||||
"error": "Unauthorized",
|
||||
"statusCode": 401
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 6,
|
||||
"phoneNumber": 89888888888,
|
||||
"firstName": "Генадий",
|
||||
"secondName": "Паровозов",
|
||||
"role": "owner"
|
||||
}
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Niwicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.qgOhk9tNcaMRbarRWISTgvGx5Eq_X8fcA5lhdVs2tQI"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": 1,
|
||||
"role": "dogsitter"
|
||||
}
|
||||
5
server/routers/dogsitters-finder/json/role/error.json
Normal file
5
server/routers/dogsitters-finder/json/role/error.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"message": "Неверный jwt token",
|
||||
"error": "Forbidden",
|
||||
"statusCode": 403
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": 3,
|
||||
"role": "owner"
|
||||
}
|
||||
@@ -1,39 +1,69 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"phone_number": 89283244141,
|
||||
"first_name": "Вася",
|
||||
"second_name": "Пупкин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
||||
"price": 1500,
|
||||
"about_me": "Я люблю собак"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"phone_number": 89272844541,
|
||||
"first_name": "Ваня",
|
||||
"second_name": "Пуськин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
|
||||
"price": 1000000,
|
||||
"about_me": "Я не люблю собак. И вообще я котоман."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"phone_number": 89872855893,
|
||||
"first_name": "Гадий",
|
||||
"second_name": "Петрович",
|
||||
"role": "owner"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"phone_number": 89872844591,
|
||||
"first_name": "Галкин",
|
||||
"second_name": "Максим",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
|
||||
"price": 1000000,
|
||||
"about_me": "Миллион алых роз"
|
||||
}
|
||||
]
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"phone_number": "89999999999",
|
||||
"first_name": "Вася",
|
||||
"second_name": "Пупкин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, Пушкина, 12",
|
||||
"price": "1500",
|
||||
"about_me": "Я люблю собак!",
|
||||
"rating": 5,
|
||||
"ratings": [
|
||||
5,
|
||||
5
|
||||
],
|
||||
"tg": "jullllllie"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"phone_number": 89272844541,
|
||||
"first_name": "Ваня",
|
||||
"second_name": "Пуськин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
|
||||
"price": 2000,
|
||||
"about_me": "Я не люблю собак. И вообще я котоман.",
|
||||
"rating": 4,
|
||||
"ratings": [
|
||||
4,
|
||||
4
|
||||
],
|
||||
"tg": "vanya006"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"phone_number": 89559999999,
|
||||
"first_name": "Гадий",
|
||||
"second_name": "Петрович",
|
||||
"role": "owner"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"phone_number": 89872844591,
|
||||
"first_name": "Галкин",
|
||||
"second_name": "Максим",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
|
||||
"price": 1750,
|
||||
"about_me": "Миллион алых роз",
|
||||
"rating": 4.5,
|
||||
"ratings": [
|
||||
4,
|
||||
5
|
||||
],
|
||||
"tg": "maks100500"
|
||||
}
|
||||
],
|
||||
"interactions": [
|
||||
{
|
||||
"owner_id": 3,
|
||||
"dogsitter_id": 4
|
||||
},
|
||||
{
|
||||
"owner_id": 1,
|
||||
"dogsitter_id": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
24
server/routers/dogsitters-finder/model/interaction.js
Normal file
24
server/routers/dogsitters-finder/model/interaction.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { Schema, model } = require("mongoose");
|
||||
|
||||
const { DSF_AUTH_USER_MODEL_NAME, DSF_INTERACTION_MODEL_NAME } = require("../../const");
|
||||
|
||||
const interactionSchema = new Schema({
|
||||
owner_id: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: DSF_AUTH_USER_MODEL_NAME,
|
||||
required: true
|
||||
},
|
||||
dogsitter_id: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: DSF_AUTH_USER_MODEL_NAME,
|
||||
required: true
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
interactionSchema.index({ owner_id: 1, dogsitter_id: 1 });
|
||||
|
||||
module.exports.Interaction = model(DSF_INTERACTION_MODEL_NAME, interactionSchema);
|
||||
83
server/routers/dogsitters-finder/model/user.js
Normal file
83
server/routers/dogsitters-finder/model/user.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const { Schema, model } = require("mongoose");
|
||||
|
||||
const { DSF_AUTH_USER_MODEL_NAME } = require("../../const");
|
||||
|
||||
const userSchema = new Schema({
|
||||
phone_number: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
match: /^\+?\d{10,15}$/
|
||||
},
|
||||
first_name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
second_name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ["dogsitter", "owner"],
|
||||
required: true
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
required: function() {
|
||||
return this.role === "dogsitter";
|
||||
}
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
required: function() {
|
||||
return this.role === "dogsitter";
|
||||
}
|
||||
},
|
||||
about_me: {
|
||||
type: String,
|
||||
maxlength: 500
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 5,
|
||||
default: 0
|
||||
},
|
||||
ratings: {
|
||||
type: [Number],
|
||||
default: [],
|
||||
validate: {
|
||||
validator: function(arr) {
|
||||
return arr.every(v => v >= 0 && v <= 5);
|
||||
},
|
||||
message: "Рейтинг должен быть в диапазоне от 0 до 5!"
|
||||
}
|
||||
},
|
||||
tg: {
|
||||
type: String,
|
||||
match: /^[a-zA-Z0-9_]{5,32}$/
|
||||
},
|
||||
created: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
userSchema.virtual("id").get(function() {
|
||||
return this._id.toHexString();
|
||||
});
|
||||
|
||||
userSchema.set("toJSON", {
|
||||
virtuals: true,
|
||||
versionKey: false,
|
||||
transform: function(doc, ret) {
|
||||
delete ret._id;
|
||||
delete ret.__v;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.User = model(DSF_AUTH_USER_MODEL_NAME, userSchema);
|
||||
149
server/routers/dogsitters-finder/routes.js
Normal file
149
server/routers/dogsitters-finder/routes.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const { Router } = require('express')
|
||||
const { expressjwt } = require('express-jwt')
|
||||
|
||||
const { getAnswer } = require('../../utils/common')
|
||||
const { User, Interaction } = require('./model')
|
||||
const { TOKEN_KEY } = require('./const')
|
||||
const { requiredValidate } = require('./utils')
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Получение списка пользователей
|
||||
router.get('/users', async (req, res) => {
|
||||
|
||||
const users = await User.find()
|
||||
.select('-__v -ratings -phone_number')
|
||||
.lean()
|
||||
|
||||
console.log('get users successfull')
|
||||
|
||||
res.send(getAnswer(null, users))
|
||||
})
|
||||
|
||||
// Получение конкретного пользователя
|
||||
router.get('/dogsitter-viewing', async (req, res) => {
|
||||
const { userId } = req.params
|
||||
|
||||
const user = await User.findById(userId)
|
||||
.select('-__v -ratings')
|
||||
.lean()
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).send(getAnswer(new Error('Пользователь не найден')))
|
||||
}
|
||||
|
||||
res.send(getAnswer(null, user))
|
||||
})
|
||||
|
||||
router.use(expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }))
|
||||
|
||||
// Добавление оценки пользователю
|
||||
router.post('/dogsitter-viewing/rating', requiredValidate('value'), async (req, res) => {
|
||||
const { userId } = req.params
|
||||
const { value } = req.body
|
||||
const authUserId = req.auth.id
|
||||
|
||||
try {
|
||||
const user = await User.findById(userId)
|
||||
if (!user) throw new Error('Пользователь не найден')
|
||||
if (user.role !== 'dogsitter') throw new Error('Нельзя оценивать этого пользователя')
|
||||
if (user.id === authUserId) throw new Error('Нельзя оценивать самого себя')
|
||||
|
||||
user.ratings.push(Number(value))
|
||||
user.rating = user.ratings.reduce((a, b) => a + b, 0) / user.ratings.length
|
||||
|
||||
const updatedUser = await user.save()
|
||||
|
||||
res.send(getAnswer(null, {
|
||||
id: updatedUser.id,
|
||||
rating: updatedUser.rating.toFixed(1),
|
||||
totalRatings: updatedUser.ratings.length
|
||||
}))
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).send(getAnswer(error))
|
||||
}
|
||||
})
|
||||
|
||||
// Обновление информации пользователя
|
||||
router.patch('/users', async (req, res) => {
|
||||
const { userId } = req.params
|
||||
const updates = req.body
|
||||
|
||||
try {
|
||||
const user = await User.findByIdAndUpdate(userId, updates, { new: true })
|
||||
.select('-__v -ratings')
|
||||
|
||||
if (!user) throw new Error('Пользователь не найден')
|
||||
res.send(getAnswer(null, user))
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).send(getAnswer(error))
|
||||
}
|
||||
})
|
||||
|
||||
// Создание объекта взаимодействия
|
||||
router.post('/interactions',
|
||||
expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }),
|
||||
requiredValidate('dogsitter_id'),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { dogsitter_id } = req.body
|
||||
const owner_id = req.auth.id // ID из JWT токена
|
||||
|
||||
// Проверка существования пользователей
|
||||
const [owner, dogsitter] = await Promise.all([
|
||||
User.findById(owner_id),
|
||||
User.findById(dogsitter_id)
|
||||
])
|
||||
|
||||
if (!owner || owner.role !== 'owner') {
|
||||
throw new Error('Владелец не найден или имеет неверную роль')
|
||||
}
|
||||
|
||||
if (!dogsitter || dogsitter.role !== 'dogsitter') {
|
||||
throw new Error('Догситтер не найден или имеет неверную роль')
|
||||
}
|
||||
|
||||
// Создание взаимодействия
|
||||
const interaction = await Interaction.create({
|
||||
owner_id,
|
||||
dogsitter_id
|
||||
})
|
||||
|
||||
res.send(getAnswer(null, {
|
||||
id: interaction.id,
|
||||
timestamp: interaction.timestamp
|
||||
}))
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).send(getAnswer(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
router.get('/interactions/check', async (req, res) => {
|
||||
const { owner_id, dogsitter_id } = req.query;
|
||||
|
||||
if (!owner_id || !dogsitter_id) {
|
||||
return res.status(400).send(getAnswer('Missing owner_id or dogsitter_id'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Поиск взаимодействий по owner_id и dogsitter_id
|
||||
const interactions = await Interaction.find({ owner_id, dogsitter_id })
|
||||
.select('-__v') // Выбираем только нужные поля
|
||||
.lean();
|
||||
|
||||
if (interactions.length === 0) {
|
||||
return res.status(404).send(getAnswer('No interactions found'));
|
||||
}
|
||||
|
||||
res.send(getAnswer(null, interactions));
|
||||
} catch (error) {
|
||||
console.error('Error checking interactions:', error);
|
||||
res.status(500).send(getAnswer('Internal Server Error'));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router
|
||||
@@ -1,111 +1,117 @@
|
||||
const router = require('express').Router()
|
||||
const {MasterModel} = require('./model/master')
|
||||
const mongoose = require("mongoose")
|
||||
const {OrderModel} = require("./model/order")
|
||||
const router = require("express").Router();
|
||||
const { MasterModel } = require("./model/master");
|
||||
const mongoose = require("mongoose");
|
||||
const { OrderModel } = require("./model/order");
|
||||
|
||||
router.post("/masters/list", async (req, res, next) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.body;
|
||||
|
||||
router.get("/masters", async (req, res, next) => {
|
||||
try {
|
||||
const masters = await MasterModel.find({});
|
||||
const orders = await OrderModel.find({});
|
||||
|
||||
const mastersWithOrders = masters.map((master) => {
|
||||
const masterOrders = orders.filter((order) => {
|
||||
return (
|
||||
order?.master && order.master.toString() === master._id.toString()
|
||||
);
|
||||
});
|
||||
|
||||
const schedule = masterOrders.map((order) => ({
|
||||
id: order._id,
|
||||
startWashTime: order.startWashTime,
|
||||
endWashTime: order.endWashTime,
|
||||
}));
|
||||
|
||||
return {
|
||||
id: master._id,
|
||||
name: master.name,
|
||||
schedule: schedule,
|
||||
phone: master.phone,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).send({ success: true, body: mastersWithOrders });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/masters/:id', async (req, res,next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)){
|
||||
throw new Error('ID is required')
|
||||
if (!startDate || !endDate) {
|
||||
throw new Error("Missing startDate or endDate");
|
||||
}
|
||||
|
||||
try {
|
||||
const master = await MasterModel.findByIdAndDelete(id, {
|
||||
new: true,
|
||||
});
|
||||
if (!master) {
|
||||
throw new Error('master not found')
|
||||
}
|
||||
res.status(200).send({success: true, body: master})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const masters = await MasterModel.find({});
|
||||
|
||||
const orders = await OrderModel.find({
|
||||
$or: [
|
||||
{ startWashTime: { $lt: end }, endWashTime: { $gt: start } }
|
||||
]
|
||||
});
|
||||
|
||||
router.post('/masters', async (req, res,next) => {
|
||||
|
||||
const {name, phone} = req.body
|
||||
|
||||
if (!name || !phone ){
|
||||
throw new Error('Enter name and phone')
|
||||
}
|
||||
try {
|
||||
const master = await MasterModel.create({name, phone})
|
||||
res.status(200).send({success: true, body: master})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
router.patch('/masters/:id', async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error('ID is required')
|
||||
}
|
||||
|
||||
const { name, phone } = req.body;
|
||||
|
||||
if (!name && !phone) {
|
||||
throw new Error('Enter name and phone')
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const updateData = {};
|
||||
if (name) updateData.name = name;
|
||||
if (phone) updateData.phone = phone;
|
||||
|
||||
const master = await MasterModel.findByIdAndUpdate(
|
||||
id,
|
||||
updateData,
|
||||
{ new: true }
|
||||
const mastersWithOrders = masters.map((master) => {
|
||||
const masterOrders = orders.filter((order) => {
|
||||
return (
|
||||
order?.master && order.master.toString() === master._id.toString()
|
||||
);
|
||||
});
|
||||
|
||||
if (!master) {
|
||||
throw new Error('master not found')
|
||||
}
|
||||
const schedule = masterOrders.map((order) => ({
|
||||
id: order._id,
|
||||
startWashTime: order.startWashTime,
|
||||
endWashTime: order.endWashTime,
|
||||
}));
|
||||
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
return {
|
||||
id: master._id,
|
||||
name: master.name,
|
||||
schedule: schedule,
|
||||
phone: master.phone,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).send({ success: true, body: mastersWithOrders });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router
|
||||
router.delete("/masters/:id", async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error("ID is required");
|
||||
}
|
||||
|
||||
try {
|
||||
const master = await MasterModel.findByIdAndDelete(id, {
|
||||
new: true,
|
||||
});
|
||||
if (!master) {
|
||||
throw new Error("master not found");
|
||||
}
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/masters", async (req, res, next) => {
|
||||
const { name, phone } = req.body;
|
||||
|
||||
if (!name || !phone) {
|
||||
throw new Error("Enter name and phone");
|
||||
}
|
||||
try {
|
||||
const master = await MasterModel.create({ name, phone });
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch("/masters/:id", async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error("ID is required");
|
||||
}
|
||||
|
||||
const { name, phone } = req.body;
|
||||
|
||||
if (!name && !phone) {
|
||||
throw new Error("Enter name and phone");
|
||||
}
|
||||
|
||||
try {
|
||||
const updateData = {};
|
||||
if (name) updateData.name = name;
|
||||
if (phone) updateData.phone = phone;
|
||||
|
||||
const master = await MasterModel.findByIdAndUpdate(id, updateData, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (!master) {
|
||||
throw new Error("master not found");
|
||||
}
|
||||
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
23
server/routers/dry-wash/get-token.js
Normal file
23
server/routers/dry-wash/get-token.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const getGigaToken = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||
const data = await response.json()
|
||||
return data.features['dry-wash-bh'].GIGA_TOKEN.value
|
||||
}
|
||||
|
||||
const getSystemPrompt = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||
const data = await response.json()
|
||||
return data.features['dry-wash-bh'].SYSTEM_PROMPT.value
|
||||
}
|
||||
|
||||
const getGigaChatModel = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||
const data = await response.json()
|
||||
return data.features['dry-wash-bh'].GIGA_CHAT_MODEL.value
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGigaToken,
|
||||
getSystemPrompt,
|
||||
getGigaChatModel
|
||||
}
|
||||
29
server/routers/dry-wash/model/order.car-img.js
Normal file
29
server/routers/dry-wash/model/order.car-img.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { Schema, model } = require('mongoose')
|
||||
|
||||
const schema = new Schema({
|
||||
image: String,
|
||||
imageRating: String,
|
||||
imageDescription: String,
|
||||
orderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'dry-wash-order'
|
||||
},
|
||||
created: {
|
||||
type: Date,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
schema.set('toJSON', {
|
||||
virtuals: true,
|
||||
versionKey: false,
|
||||
transform(_doc, ret) {
|
||||
delete ret._id
|
||||
}
|
||||
})
|
||||
|
||||
schema.virtual('id').get(function () {
|
||||
return this._id.toHexString()
|
||||
})
|
||||
|
||||
exports.OrderCarImgModel = model('dry-wash-order-car-image', schema)
|
||||
@@ -1,5 +1,6 @@
|
||||
const { Schema, model } = require('mongoose')
|
||||
const { orderStatus } = require('./const')
|
||||
const { OrderNumberModel } = require('./order.number')
|
||||
|
||||
const schema = new Schema({
|
||||
phone: {
|
||||
@@ -14,7 +15,7 @@ const schema = new Schema({
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
carColor: String,
|
||||
carColor: Schema.Types.Mixed,
|
||||
startWashTime: {
|
||||
type: Date,
|
||||
required: true
|
||||
@@ -27,6 +28,10 @@ const schema = new Schema({
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
orderNumber: {
|
||||
type: String,
|
||||
unique: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -47,6 +52,18 @@ const schema = new Schema({
|
||||
},
|
||||
})
|
||||
|
||||
schema.pre('save', async function (next) {
|
||||
if (this.isNew) {
|
||||
const counter = await OrderNumberModel.findOneAndUpdate(
|
||||
{ _id: 'orderNumber' },
|
||||
{ $inc: { sequenceValue: 1 } },
|
||||
{ new: true, upsert: true }
|
||||
)
|
||||
this.orderNumber = counter.sequenceValue.toString()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
schema.set('toJSON', {
|
||||
virtuals: true,
|
||||
versionKey: false,
|
||||
|
||||
14
server/routers/dry-wash/model/order.number.js
Normal file
14
server/routers/dry-wash/model/order.number.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { Schema, model } = require('mongoose')
|
||||
|
||||
const schema = new Schema({
|
||||
_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sequenceValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
exports.OrderNumberModel = model('dry-wash-order-number', schema)
|
||||
@@ -1,13 +1,23 @@
|
||||
const mongoose = require("mongoose")
|
||||
const router = require('express').Router()
|
||||
const multer = require('multer')
|
||||
const { MasterModel } = require('./model/master')
|
||||
const { OrderModel } = require('./model/order')
|
||||
const { OrderCarImgModel } = require('./model/order.car-img')
|
||||
const { orderStatus } = require('./model/const')
|
||||
const { getGigaToken, getSystemPrompt, getGigaChatModel } = require('./get-token')
|
||||
|
||||
const isValidPhoneNumber = (value) => /^(\+)?\d{9,15}/.test(value)
|
||||
const isValidCarNumber = (value) => /^[авекмнорстух][0-9]{3}[авекмнорстух]{2}[0-9]{2,3}$/i.test(value)
|
||||
const isValidCarBodyType = (value) => typeof value === 'number' && value > 0 && value < 100
|
||||
const isValidCarColor = (value) => value.length < 50 && /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
|
||||
const isValidCarColor = (value) => {
|
||||
if (typeof value === 'number') {
|
||||
return value >= 0 && value <= 7
|
||||
} else if (typeof value === 'string') {
|
||||
return /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
|
||||
}
|
||||
return false
|
||||
}
|
||||
const isValidISODate = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/.test(value)
|
||||
|
||||
const latitudeRe = /^(-?[1-8]?\d(?:\.\d{1,18})?|90(?:\.0{1,18})?)$/
|
||||
@@ -26,6 +36,9 @@ const isValidLocation = (value) => {
|
||||
const isValidOrderStatus = (value) => Object.values(orderStatus).includes(value)
|
||||
const isValidOrderNotes = (value) => value.length < 500
|
||||
|
||||
const allowedMimeTypes = ['image/jpeg', 'image/png']
|
||||
const sizeLimitInMegaBytes = 15
|
||||
|
||||
const VALIDATION_MESSAGES = {
|
||||
order: {
|
||||
notFound: 'Order not found'
|
||||
@@ -60,6 +73,13 @@ const VALIDATION_MESSAGES = {
|
||||
carColor: {
|
||||
invalid: 'Invalid car color'
|
||||
},
|
||||
carImg: {
|
||||
required: 'Car image file is required',
|
||||
invalid: {
|
||||
type: `Invalid car image file type. Allowed types: ${allowedMimeTypes}`,
|
||||
size: `Invalid car image file size. Limit is ${sizeLimitInMegaBytes}MB`
|
||||
}
|
||||
},
|
||||
washingBegin: {
|
||||
required: 'Begin time of washing is required',
|
||||
invalid: 'Invalid begin time of washing'
|
||||
@@ -143,17 +163,21 @@ router.post('/create', async (req, res, next) => {
|
||||
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
const { id } = req.params
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await OrderModel.findById(id)
|
||||
|
||||
if (!order) {
|
||||
throw new Error(VALIDATION_MESSAGES.order.notFound)
|
||||
}
|
||||
|
||||
res.status(200).send({ success: true, body: order })
|
||||
const imgProps = await OrderCarImgModel.findOne({ orderId: order.id })
|
||||
|
||||
res.status(200).send({ success: true, body: { ...order.toObject(), ...imgProps?.toObject() } })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
@@ -248,4 +272,167 @@ router.delete('/:id', async (req, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
const storage = multer.memoryStorage()
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: { fileSize: sizeLimitInMegaBytes * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||
cb(null, true)
|
||||
} else {
|
||||
cb(new Error(VALIDATION_MESSAGES.carImg.invalid.type), false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
const axios = require('axios')
|
||||
|
||||
const GIGA_CHAT_OAUTH = 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth'
|
||||
const GIGA_CHAT_API = 'https://gigachat.devices.sberbank.ru/api/v1'
|
||||
|
||||
const getToken = async (req, res) => {
|
||||
const gigaToken = await getGigaToken()
|
||||
|
||||
const rqUID = uuidv4()
|
||||
const body = new URLSearchParams({
|
||||
scope: "GIGACHAT_API_PERS",
|
||||
})
|
||||
|
||||
const response = await fetch(GIGA_CHAT_OAUTH, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${gigaToken}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
RqUID: rqUID,
|
||||
},
|
||||
body,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
console.error("Ошибка запроса: ", errorData)
|
||||
return res.status(response.status).json(errorData)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
const uploadImage = async (file, accessToken) => {
|
||||
const formData = new FormData()
|
||||
const blob = new Blob([file.buffer], { type: file.mimetype })
|
||||
formData.append('file', blob, file.originalname)
|
||||
formData.append('purpose', 'general')
|
||||
|
||||
const config = {
|
||||
maxBodyLength: Infinity,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${GIGA_CHAT_API}/files`, formData, config)
|
||||
return response.data.id
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const analyzeImage = async (fileId, token) => {
|
||||
const response = await fetch(`${GIGA_CHAT_API}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: (await getGigaChatModel()) ?? "GigaChat-Max",
|
||||
stream: false,
|
||||
update_interval: 0,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: (await getSystemPrompt()) ?? `Ты эксперт по оценке степени загрязнения автомобилей. Твоя задача — анализировать фотографии машин и определять степень их загрязнения. ВАЖНО: Твой ответ ДОЛЖЕН быть СТРОГО в формате JSON и содержать ТОЛЬКО следующие поля: { "value": число от 0 до 10 (целое или с одним знаком после запятой), "description": "текстовое описание на русском языке" } Правила: 1. Поле "value": - Должно быть числом от 0 до 10 - 0 = машина абсолютно чистая - 10 = машина максимально грязная 2. Поле "description": - Должно содержать 2-3 предложения на русском языке - Обязательно указать конкретные признаки загрязнения - Объяснить почему выставлен именно такой балл НЕ ДОБАВЛЯЙ никаких дополнительных полей или комментариев вне JSON структуры. НЕ ИСПОЛЬЗУЙ markdown форматирование. ОТВЕТ ДОЛЖЕН БЫТЬ ВАЛИДНЫМ JSON. Если на фотографии нет одной машины, то оценка должна быть 0 и в описании должно быть указано, почему не удалось оценить.`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: 'Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке',
|
||||
attachments: [fileId],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
console.log(data)
|
||||
|
||||
try {
|
||||
return JSON.parse(data.choices[0].message.content)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { description: data.choices[0].message.content }
|
||||
}
|
||||
}
|
||||
|
||||
const convertFileToBase64 = (file) => {
|
||||
const base64Image = file.buffer.toString('base64')
|
||||
return `data:${file.mimetype};base64,${base64Image}`
|
||||
}
|
||||
|
||||
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
|
||||
|
||||
router.post('/:id/upload-car-img', upload.single('file'), async (req, res) => {
|
||||
const { id: orderId } = req.params
|
||||
if (!mongoose.Types.ObjectId.isValid(orderId)) {
|
||||
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
|
||||
}
|
||||
const order = await OrderModel.findById(orderId)
|
||||
if (!order) {
|
||||
throw new Error(VALIDATION_MESSAGES.order.notFound)
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
throw new Error(VALIDATION_MESSAGES.carImg.required)
|
||||
}
|
||||
|
||||
try {
|
||||
await OrderCarImgModel.deleteMany({ orderId })
|
||||
|
||||
const { access_token } = await getToken(req, res)
|
||||
|
||||
const fileId = await uploadImage(req.file, access_token)
|
||||
const { value, description } = await analyzeImage(fileId, access_token) ?? {}
|
||||
|
||||
const orderCarImg = await OrderCarImgModel.create({
|
||||
image: convertFileToBase64(req.file),
|
||||
imageRating: value,
|
||||
imageDescription: description,
|
||||
orderId: order.id,
|
||||
created: new Date().toISOString(),
|
||||
})
|
||||
|
||||
res.status(200).send({ success: true, body: orderCarImg })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
router.use((err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
switch (err.message) {
|
||||
case 'File too large':
|
||||
return res.status(400).json({ success: false, error: VALIDATION_MESSAGES.carImg.invalid.size })
|
||||
default:
|
||||
return res.status(400).json({ success: false, error: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(err.message)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -8,22 +8,44 @@ router.get("/update-like", (request, response) => {
|
||||
response.send(require("./json/gamepage/success.json"));
|
||||
});
|
||||
|
||||
router.get("/categories", (request, response) => {
|
||||
response.send(require("./json/categories/success.json"));
|
||||
router.get("/add-to-cart", (request, response) => {
|
||||
response.send(require("./json/home-page-data/games-in-cart.json"));
|
||||
});
|
||||
|
||||
router.get("/categories", (request, response) => {
|
||||
response.send(require("./json/home-page-data/all-games.json"));
|
||||
});
|
||||
|
||||
router.get("/favourites", (request, response) => {
|
||||
response.send(require("./json/home-page-data/all-games.json"));
|
||||
});
|
||||
|
||||
// router.get("/shopping-cart", (request, response) => {
|
||||
// response.send(require("./json/shopping-cart/success.json"));
|
||||
// });
|
||||
|
||||
router.get("/shopping-cart", (request, response) => {
|
||||
response.send(require("./json/shopping-cart/success.json"));
|
||||
response.send(require("./json/home-page-data/games-in-cart.json"));
|
||||
});
|
||||
|
||||
router.get("/home", (request, response) => {
|
||||
response.send(require("./json/home-page-data/success.json"));
|
||||
// Добавляем поддержку разных ответов для /home
|
||||
router.get("/home", (req, res) => {
|
||||
if (stubs.home === "success") {
|
||||
res.send(require("./json/home-page-data/success.json"));
|
||||
} else if (stubs.home === "empty") {
|
||||
res.send({ data: [] }); // Отправляем пустой массив
|
||||
} else {
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/all-games", (request, response) => {
|
||||
response.send(require("./json/home-page-data/all-games.json"));
|
||||
});
|
||||
|
||||
const stubs = {
|
||||
home: "success",
|
||||
};
|
||||
|
||||
// // Маршрут для обновления лайков
|
||||
// router.post("/update-like", (request, response) => {
|
||||
@@ -38,7 +60,6 @@ router.get("/all-games", (request, response) => {
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
@@ -49,7 +70,7 @@ const commentsFilePath = path.join(__dirname, "./json/gamepage/success.json");
|
||||
async function readComments() {
|
||||
const data = await fs.readFile(commentsFilePath, "utf-8");
|
||||
const parsedData = JSON.parse(data);
|
||||
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
|
||||
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
|
||||
return parsedData;
|
||||
}
|
||||
// Write to JSON file
|
||||
@@ -88,5 +109,149 @@ router.post("/update-like", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Путь к JSON-файлу с корзиной
|
||||
const cartFilePath = path.join(
|
||||
__dirname,
|
||||
"./json/home-page-data/games-in-cart.json"
|
||||
);
|
||||
|
||||
// Функция для чтения JSON-файла
|
||||
async function readCart() {
|
||||
const data = await fs.readFile(cartFilePath, "utf-8");
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
// Функция для записи в JSON-файл
|
||||
async function writeCart(data) {
|
||||
await fs.writeFile(cartFilePath, JSON.stringify(data, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
// Маршрут для добавления/удаления товара в корзине
|
||||
router.post("/add-to-cart", async (req, res) => {
|
||||
const { id, action } = req.body;
|
||||
|
||||
// Проверка наличия id и action
|
||||
if (id === undefined || action === undefined) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "Invalid id or action" });
|
||||
}
|
||||
|
||||
try {
|
||||
const cartData = await readCart();
|
||||
let ids = cartData.data.ids;
|
||||
|
||||
if (action === "add") {
|
||||
// Если action "add", добавляем товар, если его нет в корзине
|
||||
if (!ids?.includes(id)) {
|
||||
ids.push(id);
|
||||
}
|
||||
} else if (action === "remove") {
|
||||
// Если action "remove", удаляем товар, если он есть в корзине
|
||||
if (ids?.includes(id)) {
|
||||
ids = ids.filter((item) => item !== id);
|
||||
}
|
||||
} else {
|
||||
// Если action невалиден
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "Invalid action" });
|
||||
}
|
||||
|
||||
// Записываем обновленные данные обратно в файл
|
||||
cartData.data.ids = ids;
|
||||
await writeCart(cartData);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Cart updated successfully",
|
||||
data: cartData.data, // Возвращаем обновленные данные
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating cart:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
const createElement = (key, value, buttonTitle, basePath) => `
|
||||
<label>
|
||||
<input name="${key}" type="radio" ${
|
||||
stubs[key] === value ? "checked" : ""
|
||||
} onclick="fetch('${basePath}/admin/set/${key}/${value}')"/>
|
||||
${buttonTitle || value}
|
||||
</label>
|
||||
`;
|
||||
|
||||
router.get("/admin/home", (request, response) => {
|
||||
const basePath = request.baseUrl; // Получаем базовый путь маршрутизатора
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /home</legend>
|
||||
${createElement("home", "success", "Отдать успешный ответ", basePath)}
|
||||
${createElement("home", "empty", "Отдать пустой массив", basePath)}
|
||||
${createElement("home", "error", "Отдать ошибку", basePath)}
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/game-page", (request, response) => {
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /game-page</legend>
|
||||
${createElement(
|
||||
"game-page",
|
||||
"success",
|
||||
"Отдать успешный ответ"
|
||||
)}
|
||||
${createElement("game-page", "empty", "Отдать пустой массив")}
|
||||
${createElement("game-page", "error", "Отдать ошибку")}
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/categories", (request, response) => {
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /categories</legend>
|
||||
${createElement(
|
||||
"categories",
|
||||
"success",
|
||||
"Отдать успешный ответ"
|
||||
)}
|
||||
${createElement("categories", "empty", "Отдать пустой массив")}
|
||||
${createElement("categories", "error", "Отдать ошибку")}
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/favourites", (request, response) => {
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /favourites</legend>
|
||||
${createElement(
|
||||
"favourites",
|
||||
"success",
|
||||
"Отдать успешный ответ"
|
||||
)}
|
||||
${createElement("favourites", "empty", "Отдать пустой массив")}
|
||||
${createElement("favourites", "error", "Отдать ошибку")}
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/set/:key/:value", (request, response) => {
|
||||
const { key, value } = request.params;
|
||||
stubs[key] = value;
|
||||
response.send("Настройки обновлены!");
|
||||
});
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"games1": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "How to Survive",
|
||||
"price": 259,
|
||||
"old_price": 500,
|
||||
"image": "sales_game1",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Red Solstice 2 Survivors",
|
||||
"price": 561,
|
||||
"image": "sales_game2",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Sons Of The Forests",
|
||||
"price": 820,
|
||||
"old_price": 1100,
|
||||
"image": "new_game2",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "The Witcher 3: Wild Hunt",
|
||||
"price": 990,
|
||||
"old_price": 1200,
|
||||
"image": "leaders_game4",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Atomic Heart",
|
||||
"price": 1200,
|
||||
"old_price": 2500,
|
||||
"image": "leaders_game5",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Crab Game",
|
||||
"price": 600,
|
||||
"old_price": 890,
|
||||
"image": "leaders_game6",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
}
|
||||
],
|
||||
"games2": [
|
||||
{
|
||||
"id": 7,
|
||||
"title": "Alpha League",
|
||||
"price": 299,
|
||||
"image": "new_game1",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "Sons Of The Forests",
|
||||
"price": 820,
|
||||
"old_price": 1100,
|
||||
"image": "new_game2",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "Pacific Drives",
|
||||
"price": 1799,
|
||||
"image": "new_game3",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "The Witcher 3: Wild Hunt",
|
||||
"price": 990,
|
||||
"old_price": 1200,
|
||||
"image": "leaders_game4",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Atomic Heart",
|
||||
"price": 1200,
|
||||
"old_price": 2500,
|
||||
"image": "leaders_game5",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Crab Game",
|
||||
"price": 600,
|
||||
"old_price": 890,
|
||||
"image": "leaders_game6",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
}
|
||||
],
|
||||
"games3": [
|
||||
{
|
||||
"id": 10,
|
||||
"title": "Elden Ring",
|
||||
"price": 3295,
|
||||
"old_price": 3599,
|
||||
"image": "leaders_game2",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title": "Counter-Strike 2",
|
||||
"price": 479,
|
||||
"image": "leaders_game1",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "PUBG: BATTLEGROUNDS",
|
||||
"price": 199,
|
||||
"image": "leaders_game3",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "The Witcher 3: Wild Hunt",
|
||||
"price": 990,
|
||||
"old_price": 1200,
|
||||
"image": "leaders_game4",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Atomic Heart",
|
||||
"price": 1200,
|
||||
"old_price": 2500,
|
||||
"image": "leaders_game5",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Crab Game",
|
||||
"price": 600,
|
||||
"old_price": 890,
|
||||
"image": "leaders_game6",
|
||||
"os": "windows",
|
||||
"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -5,28 +5,28 @@
|
||||
{
|
||||
"username": "Пользователь1",
|
||||
"text": "Текст комментария 1",
|
||||
"likes": 9,
|
||||
"likes": 13,
|
||||
"rating": 8,
|
||||
"date": "2025-03-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"username": "Пользователь2",
|
||||
"text": "Текст комментария 2",
|
||||
"likes": 7,
|
||||
"likes": 10,
|
||||
"rating": 7,
|
||||
"date": "2025-01-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"username": "Пользователь3",
|
||||
"text": "Текст комментария 3",
|
||||
"likes": 5,
|
||||
"likes": 4,
|
||||
"rating": 3,
|
||||
"date": "2025-02-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"username": "Пользователь4",
|
||||
"text": "Текст комментария 4",
|
||||
"likes": 15,
|
||||
"likes": 18,
|
||||
"rating": 2,
|
||||
"date": "2025-12-01T10:00:00Z"
|
||||
}
|
||||
|
||||
@@ -3,147 +3,186 @@
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "The Witcher 3: Wild Hunt",
|
||||
"image": "game1",
|
||||
"text": "$10",
|
||||
"imgPath": "img_top_1",
|
||||
"description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.",
|
||||
"title": "Elden Ring",
|
||||
"image": "game17",
|
||||
"price": 3295,
|
||||
"old_price": 3599,
|
||||
"imgPath": "img_top_17",
|
||||
"description": "Крупномасштабная RPG, действие которой происходит в обширном открытом мире c богатой мифологией и множеством опасных врагов.",
|
||||
"category": "RPG"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Red Dead Redemption 2",
|
||||
"title": "The Witcher 3: Wild Hunt",
|
||||
"image": "game1",
|
||||
"price": 990,
|
||||
"old_price": 1200,
|
||||
"imgPath": "img_top_1",
|
||||
"description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.",
|
||||
"category": "RPG"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"title": "Red Dead Redemption 2",
|
||||
"image": "game2",
|
||||
"text": "$10",
|
||||
"price": 980,
|
||||
"old_price": 3800,
|
||||
"imgPath": "img_top_2",
|
||||
"description": "Приключенческая игра с открытым миром на Диком Западе, рассказывающая историю Артура Моргана.",
|
||||
"category": "Adventures"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Forza Horizon 5",
|
||||
"title": "Forza Horizon 5",
|
||||
"image": "game3",
|
||||
"text": "$10",
|
||||
"price": 1900,
|
||||
"imgPath": "img_top_3",
|
||||
"description": "Гоночная игра с огромным открытым миром, действие которой происходит в Мексике.",
|
||||
"category": "Race"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Atomic Heart",
|
||||
"title": "Atomic Heart",
|
||||
"image": "game4",
|
||||
"text": "$10",
|
||||
"price": 1200,
|
||||
"old_price": 2500,
|
||||
|
||||
"imgPath": "img_top_4",
|
||||
"description": "Экшен-шутер с элементами RPG, разворачивающийся в альтернативной Советской России.",
|
||||
"category": "Shooters"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Counter-Strike 2",
|
||||
"title": "Counter-Strike 2",
|
||||
"image": "game5",
|
||||
"text": "$10",
|
||||
"price": 479,
|
||||
|
||||
"imgPath": "img_top_5",
|
||||
"description": "Популярный онлайн-шутер с соревновательным геймплеем и тактическими элементами.",
|
||||
"category": "Shooters"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Grand Theft Auto V",
|
||||
"title": "Grand Theft Auto V",
|
||||
"image": "game6",
|
||||
"text": "$10",
|
||||
"price": 700,
|
||||
|
||||
"imgPath": "img_top_6",
|
||||
"description": "Игра с открытым миром, где можно погрузиться в криминальный мир Лос-Сантоса.",
|
||||
"category": "Adventures"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Assassin’s Creed IV: Black Flag",
|
||||
"title": "Assassin’s Creed IV: Black Flag",
|
||||
"image": "game7",
|
||||
"text": "$10",
|
||||
"price": 1100,
|
||||
|
||||
"imgPath": "img_top_7",
|
||||
"description": "Приключенческая игра о пиратах и морских сражениях в эпоху золотого века пиратства.",
|
||||
"category": "Adventures"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Spider-Man",
|
||||
"title": "Spider-Man",
|
||||
"image": "game8",
|
||||
"text": "$10",
|
||||
"price": 3800,
|
||||
|
||||
"imgPath": "img_top_8",
|
||||
"description": "Игра о супергерое Человеке-пауке с захватывающими битвами и паркуром по Нью-Йорку.",
|
||||
"category": "Action"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Assassin’s Creed Mirage",
|
||||
"title": "Assassin’s Creed Mirage",
|
||||
"image": "game9",
|
||||
"text": "$10",
|
||||
"price": 1600,
|
||||
|
||||
"imgPath": "img_top_9",
|
||||
"description": "Приключенческая игра с упором на скрытность, вдохновленная классическими частями серии.",
|
||||
"category": "Action"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Assassin’s Creed Valhalla",
|
||||
"title": "Assassin’s Creed Valhalla",
|
||||
"image": "game10",
|
||||
"text": "$10",
|
||||
"price": 800,
|
||||
"old_price": 2200,
|
||||
|
||||
"imgPath": "img_top_10",
|
||||
"description": "RPG с открытым миром о викингах, включающая битвы, исследования и строительство поселений.",
|
||||
"category": "RPG"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "ARK: Survival Evolved",
|
||||
"title": "ARK: Survival Evolved",
|
||||
"image": "game11",
|
||||
"text": "$10",
|
||||
"price": 790,
|
||||
|
||||
"imgPath": "img_top_11",
|
||||
"description": "Выживание в открытом мире с динозаврами, строительством и многопользовательскими элементами.",
|
||||
"category": "Simulators"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "FIFA 23",
|
||||
"title": "FIFA 23",
|
||||
"image": "game12",
|
||||
"text": "$10",
|
||||
"price": 3900,
|
||||
|
||||
"imgPath": "img_top_12",
|
||||
"description": "Популярный футбольный симулятор с улучшенной графикой и реалистичным геймплеем.",
|
||||
"category": "Sports"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Dirt 5",
|
||||
"title": "Dirt 5",
|
||||
"image": "game13",
|
||||
"text": "$10",
|
||||
"price": 2300,
|
||||
|
||||
"imgPath": "img_top_13",
|
||||
"description": "Аркадная гоночная игра с фокусом на ралли и внедорожных соревнованиях.",
|
||||
"category": "Race"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Cyberpunk 2077",
|
||||
"title": "Cyberpunk 2077",
|
||||
"image": "game14",
|
||||
"text": "$10",
|
||||
"price": 3400,
|
||||
|
||||
"imgPath": "img_top_14",
|
||||
"description": "RPG в киберпанк-сеттинге с нелинейным сюжетом и детализированным открытым миром.",
|
||||
"category": "RPG"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Age of Empires IV",
|
||||
"title": "Age of Empires IV",
|
||||
"image": "game15",
|
||||
"text": "$10",
|
||||
"price": 3200,
|
||||
|
||||
"imgPath": "img_top_15",
|
||||
"description": "Классическая стратегия в реальном времени с историческими кампаниями.",
|
||||
"category": "Strategies"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Civilization VI",
|
||||
"title": "Civilization VI",
|
||||
"image": "game16",
|
||||
"text": "$10",
|
||||
"price": 4200,
|
||||
"imgPath": "img_top_16",
|
||||
"description": "Глобальная пошаговая стратегия, в которой игроки строят и развивают цивилизации.",
|
||||
"category": "Strategies"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"ids": [
|
||||
3,
|
||||
13,
|
||||
1,
|
||||
10,
|
||||
4,
|
||||
9,
|
||||
15,
|
||||
6,
|
||||
7
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,43 +3,51 @@
|
||||
"data": {
|
||||
"topSail": [
|
||||
{
|
||||
"id": 1,
|
||||
"image": "game1",
|
||||
"text": "$10",
|
||||
"price": 1500,
|
||||
"imgPath": "img_top_1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"image": "game2",
|
||||
"text": "$10",
|
||||
"price": 980,
|
||||
"imgPath": "img_top_2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"image": "game3",
|
||||
"text": "$10",
|
||||
"price": 1900,
|
||||
"imgPath": "img_top_3"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"image": "game4",
|
||||
"text": "$10",
|
||||
"price": 1200,
|
||||
"imgPath": "img_top_4"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"image": "game5",
|
||||
"text": "$10",
|
||||
"price": 479,
|
||||
"imgPath": "img_top_5"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"image": "game6",
|
||||
"text": "$10",
|
||||
"price": 700,
|
||||
"imgPath": "img_top_6"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"image": "game7",
|
||||
"text": "$10",
|
||||
"price": 1100,
|
||||
"imgPath": "img_top_7"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"image": "game8",
|
||||
"text": "$10",
|
||||
"price": 3800,
|
||||
"imgPath": "img_top_8"
|
||||
}
|
||||
],
|
||||
@@ -97,23 +105,27 @@
|
||||
{
|
||||
"image": "news1",
|
||||
"text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare",
|
||||
"imgPath": "img_news_1"
|
||||
"imgPath": "img_news_1",
|
||||
"link": "https://gamemag.ru/news/185583/delta-force-hawk-ops-gameplay-showcase-havoc-warfare"
|
||||
},
|
||||
{
|
||||
"image": "news2",
|
||||
"text": "Первый трейлер Assassin’s Creed Shadows — с темнокожим самураем в феодальной Японии",
|
||||
"imgPath": "img_news_2"
|
||||
"imgPath": "img_news_2",
|
||||
"link": "https://stopgame.ru/newsdata/62686/pervyy_trailer_assassin_s_creed_shadows_s_temnokozhim_samuraem_v_feodalnoy_yaponii"
|
||||
},
|
||||
{
|
||||
"image": "news3",
|
||||
"text": "Призрак Цусимы» вышел на ПК — и уже ставит рекорды для Sony",
|
||||
"imgPath": "img_news_3"
|
||||
"imgPath": "img_news_3",
|
||||
"link": "https://stopgame.ru/newsdata/62706/prizrak_cusimy_vyshel_na_pk_i_uzhe_stavit_rekordy_dlya_sony"
|
||||
},
|
||||
{
|
||||
"image": "news4",
|
||||
"text": "Авторы Skull and Bones расширяют планы на второй сезо",
|
||||
"imgPath": "img_news_4"
|
||||
"text": "Авторы Skull and Bones расширяют планы на второй сезон",
|
||||
"imgPath": "img_news_4",
|
||||
"link": "https://stopgame.ru/newsdata/62711/avtory_skull_and_bones_rasshiryayut_plany_na_vtoroy_sezon"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,16 @@ router.get('/getInfoAboutKazan', (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/getServices', (request, response) => {
|
||||
const lang = request.query.lang || 'ru';
|
||||
try {
|
||||
const data = require(`./json/first/services/${lang}/success.json`);
|
||||
response.send(data);
|
||||
} catch (error) {
|
||||
response.status(404).send({ message: 'Language not found' });
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/getNews', (request, response) => {
|
||||
const lang = request.query.lang || 'ru';
|
||||
try {
|
||||
|
||||
102
server/routers/kazan-explore/json/first/services/en/success.json
Normal file
102
server/routers/kazan-explore/json/first/services/en/success.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"banks": [
|
||||
{
|
||||
"name": "Sberbank of Russia",
|
||||
"description": "One of the largest and most popular banks in Russia. There are many branches and ATMs in Kazan. Sberbank offers a wide range of services, including loans, deposits, insurance, business services, and online banking."
|
||||
},
|
||||
{
|
||||
"name": "VTB",
|
||||
"description": "The second largest bank in Russia, with many offices and ATMs in Kazan. VTB offers various financial products for individuals and businesses, including loans, deposits, investment solutions, and cards."
|
||||
},
|
||||
{
|
||||
"name": "Tinkoff Bank",
|
||||
"description": "Although Tinkoff does not have traditional offices in Kazan, its products and services are available in the city through online banking and remote services. Tinkoff offers favorable terms for credit cards, deposits, and services for small and medium-sized businesses."
|
||||
},
|
||||
{
|
||||
"name": "Alfa-Bank",
|
||||
"description": "One of the largest private banks in Russia, with offices in Kazan. Alfa-Bank offers standard banking services such as loans, deposits, cards, as well as investment and insurance products."
|
||||
},
|
||||
{
|
||||
"name": "Rosselkhozbank",
|
||||
"description": "Rosselkhozbank is also present in Kazan, specializing in servicing the agro-industrial complex, but also provides services for individuals and businesses, including loans, deposits, and cards."
|
||||
},
|
||||
{
|
||||
"name": "RBC Bank",
|
||||
"description": "A Russian bank with several offices and ATMs in Kazan. It offers loans, cards, deposits, and business services."
|
||||
},
|
||||
{
|
||||
"name": "Bank Saint Petersburg",
|
||||
"description": "A local bank that also provides services in Kazan. It offers a wide range of banking products for individuals and businesses."
|
||||
}
|
||||
],
|
||||
"hospitals": [
|
||||
{
|
||||
"name": "Kazan City Clinical Hospital No. 1",
|
||||
"description": "One of the largest multidisciplinary hospitals in Kazan, offering services in surgery, traumatology, neurology, cardiology, and other medical fields. Modern technologies and highly qualified staff."
|
||||
},
|
||||
{
|
||||
"name": "Republican Clinical Hospital",
|
||||
"description": "The main medical organization of the Republic of Tatarstan, providing a wide range of services for adults and children, including emergency care, high-tech surgeries, and diagnostics."
|
||||
},
|
||||
{
|
||||
"name": "City Hospital No. 7",
|
||||
"description": "A hospital specializing in providing medical care in therapeutic, surgical, and resuscitation medicine. The hospital employs experienced specialists and uses modern treatment methods."
|
||||
},
|
||||
{
|
||||
"name": "Kazan Children's Clinical Hospital",
|
||||
"description": "A specialized medical facility for children, providing services for the treatment of diseases related to pediatrics, surgery, cardiology, and other fields for children of all ages."
|
||||
},
|
||||
{
|
||||
"name": "Kazan Oncology Dispensary",
|
||||
"description": "A medical institution specializing in the treatment of oncological diseases. It uses the latest methods of cancer diagnosis and treatment, including chemotherapy, radiotherapy, and surgical interventions."
|
||||
},
|
||||
{
|
||||
"name": "City Hospital No. 18",
|
||||
"description": "A multidisciplinary medical institution offering treatment in various medical fields, including traumatology, neurology, and cardiology. The hospital has a rehabilitation department for patients recovering from serious diseases."
|
||||
},
|
||||
{
|
||||
"name": "Republican Hospital for War Veterans",
|
||||
"description": "A medical institution providing specialized care for World War II veterans, disabled individuals, and elderly people. It also offers a wide range of services for citizens with chronic diseases."
|
||||
}
|
||||
],
|
||||
"pharmacies": [
|
||||
{
|
||||
"name": "Apteka 36.6",
|
||||
"description": "A pharmacy chain with a wide range of medications, vitamins, cosmetics, and health products. Loyalty programs and online orders for customer convenience."
|
||||
},
|
||||
{
|
||||
"name": "Rigla",
|
||||
"description": "One of the largest pharmacy chains in Russia. It offers a wide range of medicines, medical products, and cosmetics. It also provides the option to order online."
|
||||
},
|
||||
{
|
||||
"name": "Zdorovaya Semya",
|
||||
"description": "A pharmacy chain focused on the sale of medicines and health products, including medical equipment. Often runs promotions and discounts on popular items."
|
||||
},
|
||||
{
|
||||
"name": "A5 Pharmacy Chain",
|
||||
"description": "Pharmacies offering a wide range of products, including medicines, vitamins, cosmetics, and children's products. Convenient delivery and online order services."
|
||||
},
|
||||
{
|
||||
"name": "Samson-Pharma Pharmacy",
|
||||
"description": "A pharmacy chain offering customers all necessary medicines and health products. Pharmacies offer various discount and bonus programs for regular customers."
|
||||
},
|
||||
{
|
||||
"name": "Tsvetnoy Pharmacy Chain",
|
||||
"description": "Pharmacies known for their convenient locations and high-quality service. They sell medicines, vitamins, self-care products, and medical equipment."
|
||||
},
|
||||
{
|
||||
"name": "Doctor Stoletev Pharmacy",
|
||||
"description": "A pharmacy chain focused on selling pharmaceutical products, medical goods, and cosmetics. Convenient service and promotions for customers."
|
||||
}
|
||||
],
|
||||
"airports": [
|
||||
{
|
||||
"name": "Kazan International Airport",
|
||||
"description": "The main airport of the city of Kazan, serving international and domestic flights. The airport is equipped with modern terminals, comfortable waiting areas, shops, and restaurants. It is one of the largest in the Volga region and an important transport hub for Tatarstan."
|
||||
},
|
||||
{
|
||||
"name": "Kazan-2 (when it was operational)",
|
||||
"description": "Previously used for domestic flights and military needs. It is no longer fully operational as all passenger flights have been redirected to Kazan International Airport. The airport building is closed for commercial air traffic."
|
||||
}
|
||||
]
|
||||
}
|
||||
102
server/routers/kazan-explore/json/first/services/ru/success.json
Normal file
102
server/routers/kazan-explore/json/first/services/ru/success.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"banks": [
|
||||
{
|
||||
"name": "Сбербанк России",
|
||||
"description": "Один из крупнейших и самых популярных банков в России. В Казани есть множество отделений и банкоматов. Сбербанк предлагает широкий спектр услуг, включая кредиты, депозиты, страхование, обслуживание бизнеса и онлайн-банкинг."
|
||||
},
|
||||
{
|
||||
"name": "ВТБ",
|
||||
"description": "Второй по величине банк в России, с большим количеством офисов и банкоматов в Казани. ВТБ предлагает различные финансовые продукты для физических и юридических лиц, включая кредиты, вклады, инвестиционные решения и карты."
|
||||
},
|
||||
{
|
||||
"name": "Тинькофф Банк",
|
||||
"description": "Несмотря на то что у Тинькофф нет традиционных офисов в Казани, его продукты и услуги доступны в городе через онлайн-банкинг и удаленное обслуживание. Тинькофф предлагает выгодные условия по кредитным картам, вклады, а также услуги для малого и среднего бизнеса."
|
||||
},
|
||||
{
|
||||
"name": "Альфа-Банк",
|
||||
"description": "Один из крупных частных банков в России, с офисами в Казани. Альфа-Банк предлагает стандартные банковские услуги, такие как кредиты, депозиты, карты, а также инвестиционные и страховые продукты."
|
||||
},
|
||||
{
|
||||
"name": "Россельхозбанк",
|
||||
"description": "В Казани также присутствует Россельхозбанк, специализирующийся на обслуживании агропромышленного комплекса, но также предоставляет услуги для физических и юридических лиц, включая кредиты, депозиты и карты."
|
||||
},
|
||||
{
|
||||
"name": "РБК Банк",
|
||||
"description": "Российский банк с рядом офисов и банкоматов в Казани. Предлагает кредиты, карты, депозиты, а также обслуживание для бизнеса."
|
||||
},
|
||||
{
|
||||
"name": "Банк Санкт-Петербург",
|
||||
"description": "Местный банк, который также предоставляет услуги в Казани. Предлагает широкий выбор банковских продуктов для частных лиц и бизнеса."
|
||||
}
|
||||
],
|
||||
"hospitals": [
|
||||
{
|
||||
"name": "Казанская городская клиническая больница №1",
|
||||
"description": "Одна из крупнейших многопрофильных больниц Казани, предлагающая услуги в области хирургии, травматологии, неврологии, кардиологии и других медицинских направлений. Современные технологии и высококвалифицированный персонал."
|
||||
},
|
||||
{
|
||||
"name": "Республиканская клиническая больница",
|
||||
"description": "Основная медицинская организация Республики Татарстан, предоставляющая широкий спектр услуг для взрослых и детей, включая экстренную помощь, высокотехнологичные операции и диагностику."
|
||||
},
|
||||
{
|
||||
"name": "Городская больница №7",
|
||||
"description": "Больница, специализирующаяся на оказании медицинской помощи в области терапевтической, хирургической и реанимационной медицины. В больнице работают опытные специалисты, используемые современные методы лечения."
|
||||
},
|
||||
{
|
||||
"name": "Казанская детская клиническая больница",
|
||||
"description": "Профильное медицинское учреждение для детей, которое предоставляет услуги по лечению заболеваний, связанных с педиатрией, хирургией, кардиологией и другими направлениями для детей всех возрастов."
|
||||
},
|
||||
{
|
||||
"name": "Казанский онкологический диспансер",
|
||||
"description": "Медицинское учреждение, специализирующееся на лечении онкологических заболеваний. Использует новейшие методы диагностики и лечения рака, включая химиотерапию, радиотерапию и операционные вмешательства."
|
||||
},
|
||||
{
|
||||
"name": "Городская больница №18",
|
||||
"description": "Многопрофильное медицинское учреждение, предлагающее лечение в различных областях медицины, включая травматологию, неврологию и кардиологию. В больнице есть отделение для реабилитации пациентов после тяжелых заболеваний."
|
||||
},
|
||||
{
|
||||
"name": "Республиканская больница для ветеранов войн",
|
||||
"description": "Медицинское учреждение, оказывающее специализированную помощь ветеранам Великой Отечественной войны, инвалидам и пожилым людям. Также предлагает широкий спектр услуг для граждан с хроническими заболеваниями."
|
||||
}
|
||||
],
|
||||
"pharmacies": [
|
||||
{
|
||||
"name": "Аптека 36,6",
|
||||
"description": "Сеть аптек с большим ассортиментом лекарственных средств, витаминов, косметики и товаров для здоровья. Программы лояльности и онлайн-заказы для удобства клиентов."
|
||||
},
|
||||
{
|
||||
"name": "Ригла",
|
||||
"description": "Одна из крупнейших аптечных сетей в России. Предлагает широкий выбор лекарств, медицинских товаров и косметики. Также предоставляет возможность заказа через интернет."
|
||||
},
|
||||
{
|
||||
"name": "Здоровая семья",
|
||||
"description": "Аптечная сеть, ориентированная на продажу лекарств и товаров для здоровья, включая медицинскую технику. Часто проводятся акции и скидки на популярные товары."
|
||||
},
|
||||
{
|
||||
"name": "Аптечная сеть 'А5'",
|
||||
"description": "Аптеки, предлагающие широкий ассортимент товаров, включая лекарства, витамины, косметику и товары для детей. Удобные услуги доставки и онлайн-заказов."
|
||||
},
|
||||
{
|
||||
"name": "Аптека 'Самсон-Фарма'",
|
||||
"description": "Сеть аптек, предоставляющая клиентам все необходимые лекарства и товары для здоровья. Аптеки предлагают различные программы скидок и бонусов для постоянных клиентов."
|
||||
},
|
||||
{
|
||||
"name": "Аптечная сеть 'Цветной'",
|
||||
"description": "Аптеки, известные своими удобными местоположениями и качественным обслуживанием. В продаже лекарства, витамины, товары для ухода за собой и медтехника."
|
||||
},
|
||||
{
|
||||
"name": "Аптека 'Доктор Столетов'",
|
||||
"description": "Сеть аптек, ориентированная на продажу фармацевтической продукции, медицинских товаров и косметики. Удобный сервис и акции для клиентов."
|
||||
}
|
||||
],
|
||||
"airports": [
|
||||
{
|
||||
"name": "Международный аэропорт Казань",
|
||||
"description": "Главный аэропорт города Казани, обслуживающий международные и внутренние рейсы. Аэропорт оснащен современными терминалами, удобными зонами ожидания, магазинами и ресторанами. Он является одним из крупнейших в Поволжье и важным транспортным узлом для Татарстана."
|
||||
},
|
||||
{
|
||||
"name": "Казань-2 (когда был действующим)",
|
||||
"description": "Ранее используемый аэропорт для внутренних рейсов и военных нужд. В настоящее время не функционирует в полном объеме, поскольку все пассажирские рейсы перенаправлены в Международный аэропорт Казань. Здание аэропорта закрыто для коммерческих авиаперевозок."
|
||||
}
|
||||
]
|
||||
}
|
||||
102
server/routers/kazan-explore/json/first/services/tt/success.json
Normal file
102
server/routers/kazan-explore/json/first/services/tt/success.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"banks": [
|
||||
{
|
||||
"name": "Россия Сбербанкы",
|
||||
"description": "Россиядәге иң зур һәм популяр банкларның берсе. Казан шәһәрендә күпсанлы бүлекләр һәм банкоматлар бар. Сбербанк киң спектрлы хезмәтләр тәкъдим итә, шул исәптән кредитлар, депозиты, иминиятләштерү, бизнеска хезмәт күрсәтү һәм онлайн-банкчылык."
|
||||
},
|
||||
{
|
||||
"name": "ВТБ",
|
||||
"description": "Россиядә икенче зурлыктагы банк, Казан шәһәрендә күп санлы офислар һәм банкоматлар белән. ВТБ физик һәм юридик затлар өчен төрле финанс продуктларын тәкъдим итә, шул исәптән кредитлар, депозитлар, инвестицион чишелешләр һәм карталар."
|
||||
},
|
||||
{
|
||||
"name": "Тинькофф Банк",
|
||||
"description": "Тинькофф Казан шәһәрендә традицион офисларга ия булмаса да, аның продуктлары һәм хезмәтләре шәһәрдә онлайн-банкчылык һәм ерак хезмәт күрсәтү аша тәкъдим ителә. Тинькофф кредит карталары, депозитлар, шулай ук кечкенә һәм урта бизнес өчен хезмәтләр тәкъдим итә."
|
||||
},
|
||||
{
|
||||
"name": "Альфа-Банк",
|
||||
"description": "Россиядәге зур шәхси банкларның берсе, Казанда офислары белән. Альфа-Банк стандарт банк хезмәтләрен тәкъдим итә, шул исәптән кредитлар, депозитлар, карталар, шулай ук инвестицион һәм иминият продуктлары."
|
||||
},
|
||||
{
|
||||
"name": "Россельхозбанк",
|
||||
"description": "Казан шәһәрендә Россельхозбанк та бар, ул агропромышленность өлкәсендә хезмәт күрсәтүгә махсуслашкан, ләкин шулай ук физик һәм юридик затлар өчен хезмәтләр тәкъдим итә, шул исәптән кредитлар, депозитлар һәм карталар."
|
||||
},
|
||||
{
|
||||
"name": "РБК Банк",
|
||||
"description": "Казан шәһәрендә офислары һәм банкоматлары булган Россия банкы. Кредитлар, карталар, депозитлар һәм бизнеска хезмәт күрсәтү тәкъдим итә."
|
||||
},
|
||||
{
|
||||
"name": "Санкт-Петербург Банкы",
|
||||
"description": "Казан шәһәрендә дә хезмәт күрсәткән җирле банк. Ул шәхси затлар һәм бизнес өчен киң банк продуктлары сайлау тәкъдим итә."
|
||||
}
|
||||
],
|
||||
"hospitals": [
|
||||
{
|
||||
"name": "Казан шәһәр клиник хастаханәсе №1",
|
||||
"description": "Казанның иң зур күппрофильле хастаханәләренең берсе, хирургия, травматология, неврология, кардиология һәм башка медицина юнәлешләре буенча хезмәтләр тәкъдим итә. Замана технологияләре һәм югары квалификацияле персонал."
|
||||
},
|
||||
{
|
||||
"name": "Республиканың клиник хастаханәсе",
|
||||
"description": "Татарстан Республикасы өчен төп медицина оешмасы, зур спектрдагы хезмәтләрне тәкъдим итә, шул исәптән ашыгыч ярдәм, югары технологияле операцияләр һәм диагностика."
|
||||
},
|
||||
{
|
||||
"name": "Шәһәр хастаханәсе №7",
|
||||
"description": "Терапевтик, хирургик һәм реанимация медицинасы өлкәсендә медицина ярдәме күрсәтүгә махсуслашкан хастаханә. Хастаханәдә тәҗрибәле белгечләр эшли, заманча дәвалау ысуллары кулланыла."
|
||||
},
|
||||
{
|
||||
"name": "Казан балалар клиник хастаханәсе",
|
||||
"description": "Балалар өчен профильле медицина учреждениесе, педиатрия, хирургия, кардиология һәм башка юнәлешләр буенча хезмәтләр тәкъдим итә."
|
||||
},
|
||||
{
|
||||
"name": "Казан онкология диспансеры",
|
||||
"description": "Онкологик авыруларны дәвалауга махсуслашкан медицина учреждениесе. Рак диагнозын һәм дәвалауны үткәрүдә заманча ысуллар кулланыла, шул исәптән химиотерапия, радиотерапия һәм операцияләр."
|
||||
},
|
||||
{
|
||||
"name": "Шәһәр хастаханәсе №18",
|
||||
"description": "Күппрофильле медицина учреждениесе, төрле медицина өлкәләрендә дәвалау тәкъдим итә, шул исәптән травматология, неврология һәм кардиология. Хастаханәдә авыр авырулардан соң реабилитация бүлекләре бар."
|
||||
},
|
||||
{
|
||||
"name": "Ветераннар өчен республика хастаханәсе",
|
||||
"description": "Бөек Ватан сугышы ветераннарына, инвалидларга һәм картларга махсус медицина ярдәме күрсәтүче учреждение. Шулай ук хроник авырулары булган гражданнар өчен хезмәтләр тәкъдим итә."
|
||||
}
|
||||
],
|
||||
"pharmacies": [
|
||||
{
|
||||
"name": "Аптека 36,6",
|
||||
"description": "Дәреслекләр, витаминнар, косметика һәм сәламәтлек товарларының киң ассортименты булган аптека челтәре. Лояльлек программалары һәм онлайн-заказлар клиентлар өчен уңайлы."
|
||||
},
|
||||
{
|
||||
"name": "Ригла",
|
||||
"description": "Россиядәге иң зур аптекалар челтәрләренең берсе. Дәреслекләр, медицина товарлары һәм косметика тәкъдим итә. Шулай ук интернет аша заказ бирү мөмкинлеге бар."
|
||||
},
|
||||
{
|
||||
"name": "Здоровая семья",
|
||||
"description": "Дәреслекләр һәм сәламәтлек товарлары, шул исәптән медицина техникасы сатуга юнәлдерелгән аптека челтәре. Популяр товарларга акцияләр һәм ташламалар еш үткәрелә."
|
||||
},
|
||||
{
|
||||
"name": "Аптечная сеть 'А5'",
|
||||
"description": "Дәреслекләр, витаминнар, косметика һәм балалар товарларының киң ассортименты булган аптека челтәре. Уңайлы җибәрү һәм онлайн-заказлар хезмәтләре."
|
||||
},
|
||||
{
|
||||
"name": "Аптека 'Самсон-Фарма'",
|
||||
"description": "Дәреслекләр һәм сәламәтлек товарлары тәкъдим итә торган аптека челтәре. Аптекалар даими клиентлар өчен скидкалар һәм бонуслар тәкъдим итә."
|
||||
},
|
||||
{
|
||||
"name": "Аптечная сеть 'Цветной'",
|
||||
"description": "Уңайлы урнашкан һәм сыйфатлы хезмәт күрсәтү белән танылган аптекалар. Дәреслекләр, витаминнар, үз-үзеңне карау товарлары һәм медицина техникасы сатыла."
|
||||
},
|
||||
{
|
||||
"name": "Аптека 'Доктор Столетов'",
|
||||
"description": "Фармацевтик продукция, медицина товарлары һәм косметика сату белән шөгыльләнгән аптека челтәре. Уңайлы хезмәт һәм клиентлар өчен акцияләр."
|
||||
}
|
||||
],
|
||||
"airports": [
|
||||
{
|
||||
"name": "Казан Халыкара Аэропорты",
|
||||
"description": "Казанның төп аэропорты, халыкара һәм эчке рейсларны башкаручы. Аэропорт заманча терминаллар, уңайлы көтү зоналары, кибетләр һәм рестораннар белән җиһазландырылган. Ул Поволжье төбәгендә иң зур аэропортларның берсе һәм Татарстан өчен мөһим транспорт узелы."
|
||||
},
|
||||
{
|
||||
"name": "Казан-2 (эшләгән вакытта)",
|
||||
"description": "Элекке эчке рейслар һәм хәрби кирәклекләр өчен кулланылган аэропорт. Хәзерге вакытта тулы көченә эшләми, чөнки барлык пассажир рейслары Казан Халыкара Аэропортына күчерелгән. Аэропорт бинасы коммерция авиаперевозкалары өчен ябык."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -788,5 +788,121 @@
|
||||
"image_url": "w_six"
|
||||
}
|
||||
]
|
||||
},
|
||||
"history": {
|
||||
"intro_text": "Let's see how well you know UNICS!",
|
||||
"intro_image": "culture",
|
||||
"questions": [
|
||||
{
|
||||
"question": "When was Kazan founded?",
|
||||
"options": [
|
||||
"1005",
|
||||
"1156",
|
||||
"1230",
|
||||
"1323"
|
||||
],
|
||||
"correct_answer": "1005",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "What is the main river flowing through Kazan?",
|
||||
"options": [
|
||||
"Volga",
|
||||
"Kazanka",
|
||||
"Kama",
|
||||
"Izh"
|
||||
],
|
||||
"correct_answer": "Kazanka",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Who was the first khan of Kazan?",
|
||||
"options": [
|
||||
"Ulugh Muhammad",
|
||||
"Akhmat",
|
||||
"Shah Ali",
|
||||
"Mamuka"
|
||||
],
|
||||
"correct_answer": "Ulugh Muhammad",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "What is the name of Kazan's main sports complex where the 2013 Universiade was held?",
|
||||
"options": [
|
||||
"Kazan Arena",
|
||||
"Tatneft Arena",
|
||||
"Bugulma Arena",
|
||||
"Ak Bars Sports Palace"
|
||||
],
|
||||
"correct_answer": "Kazan Arena",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Which mosque in Kazan is considered one of the largest in Russia?",
|
||||
"options": [
|
||||
"Kul Sharif Mosque",
|
||||
"Mari El Mosque",
|
||||
"Aisha Mosque",
|
||||
"Imam Muhammad Mosque"
|
||||
],
|
||||
"correct_answer": "Kul Sharif Mosque",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "What is the name of the square where the Kazan Kremlin and Kul Sharif Mosque are located?",
|
||||
"options": [
|
||||
"Vakhitov Square",
|
||||
"Freedom Square",
|
||||
"Kremlin Square",
|
||||
"Revolution Square"
|
||||
],
|
||||
"correct_answer": "Kremlin Square",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "What symbol of Kazan is depicted on the city's coat of arms?",
|
||||
"options": [
|
||||
"Dragon",
|
||||
"Tiger",
|
||||
"Lion",
|
||||
"Eagle"
|
||||
],
|
||||
"correct_answer": "Dragon",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Who was the architect of the Kazan Kremlin?",
|
||||
"options": [
|
||||
"Ivan Zarudny",
|
||||
"Fyodor Benjamin",
|
||||
"Andrey Ushakov",
|
||||
"Yury Dashevsky"
|
||||
],
|
||||
"correct_answer": "Andrey Ushakov",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Which of these universities is located in Kazan?",
|
||||
"options": [
|
||||
"Moscow State University",
|
||||
"Kazan Federal University",
|
||||
"St. Petersburg Polytechnic University",
|
||||
"Novosibirsk State University"
|
||||
],
|
||||
"correct_answer": "Kazan Federal University",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "What is the name of the largest street in Kazan, which is also the center of the city's nightlife?",
|
||||
"options": [
|
||||
"Kremlin Street",
|
||||
"Bauman Street",
|
||||
"Pushkin Street",
|
||||
"Kayum Nasyri Street"
|
||||
],
|
||||
"correct_answer": "Bauman Street",
|
||||
"image_url": "culture"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -788,5 +788,121 @@
|
||||
"image_url": "w_six"
|
||||
}
|
||||
]
|
||||
},
|
||||
"history": {
|
||||
"intro_text": "Давайте узнаем, насколько вы хорошо знаете историю Казани!",
|
||||
"intro_image": "culture",
|
||||
"questions": [
|
||||
{
|
||||
"question": "Когда Казань была основана?",
|
||||
"options": [
|
||||
"1000 год",
|
||||
"1156 год",
|
||||
"1230 год",
|
||||
"1323 год"
|
||||
],
|
||||
"correct_answer": "1000 год",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Как называется главная река, протекающая через Казань?",
|
||||
"options": [
|
||||
"Волга",
|
||||
"Казанка",
|
||||
"Кама",
|
||||
"Иж"
|
||||
],
|
||||
"correct_answer": "Казанка",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Кто был первым казанским ханом?",
|
||||
"options": [
|
||||
"Улу-Мухаммед",
|
||||
"Ахмат",
|
||||
"Шах-Али",
|
||||
"Мамука"
|
||||
],
|
||||
"correct_answer": "Улу-Мухаммед",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Как называется главный спортивный комплекс Казани, где проводились Универсиада 2013 года?",
|
||||
"options": [
|
||||
"Казан Арена",
|
||||
"Татнефть Арена",
|
||||
"Бугульминская арена",
|
||||
"Дворец спорта \"Ак Барс\""
|
||||
],
|
||||
"correct_answer": "Казан Арена",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Какая мечеть в Казани считается одной из самых больших в России?",
|
||||
"options": [
|
||||
"Мечеть Кул Шариф",
|
||||
"Мечеть Марий Эл",
|
||||
"Мечеть Айша",
|
||||
"Мечеть имама Мухаммада"
|
||||
],
|
||||
"correct_answer": "Мечеть Кул Шариф",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Как называется площадь, на которой расположены Казанский Кремль и мечеть Кул Шариф?",
|
||||
"options": [
|
||||
"Площадь Вахитова",
|
||||
"Площадь Свободы",
|
||||
"Площадь Кремля",
|
||||
"Площадь Революции"
|
||||
],
|
||||
"correct_answer": "Площадь Кремля",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Какой символ Казани изображён на гербе города?",
|
||||
"options": [
|
||||
"Дракон",
|
||||
"Тигр",
|
||||
"Лев",
|
||||
"Орел"
|
||||
],
|
||||
"correct_answer": "Дракон",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Какой архитектор спроектировал Казанский Кремль?",
|
||||
"options": [
|
||||
"Иван Зарудный",
|
||||
"Фёдор Бенжамин",
|
||||
"Андрей Ушаков",
|
||||
"Юрий Дашевский"
|
||||
],
|
||||
"correct_answer": "Андрей Ушаков",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Какой из этих вузов находится в Казани?",
|
||||
"options": [
|
||||
"Московский государственный университет",
|
||||
"Казанский федеральный университет",
|
||||
"Санкт-Петербургский политехнический университет",
|
||||
"Новосибирский государственный университет"
|
||||
],
|
||||
"correct_answer": "Казанский федеральный университет",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Как называется крупнейшая в Казани улица, которая также является центром ночной жизни города?",
|
||||
"options": [
|
||||
"Кремлевская улица",
|
||||
"Баумана улица",
|
||||
"Пушкина улица",
|
||||
"Каюма Насыри улица"
|
||||
],
|
||||
"correct_answer": "Баумана улица",
|
||||
"image_url": "culture"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -788,5 +788,121 @@
|
||||
"image_url": "w_six"
|
||||
}
|
||||
]
|
||||
},
|
||||
"history": {
|
||||
"intro_text": "Әйдәгез, Казан тарихын белүегезне ачыклыйк!",
|
||||
"intro_image": "culture",
|
||||
"questions": [
|
||||
{
|
||||
"question": "Казан кайчан нигезләнгән?",
|
||||
"options": [
|
||||
"1000 ел",
|
||||
"1156 ел",
|
||||
"1230 ел",
|
||||
"1323 ел"
|
||||
],
|
||||
"correct_answer": "1000 ел",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Казан аша агучы төп елга ничек атала?",
|
||||
"options": [
|
||||
"Идел",
|
||||
"Казанка",
|
||||
"Кама",
|
||||
"Иж"
|
||||
],
|
||||
"correct_answer": "Казанка",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Казанның беренче ханы кем булган?",
|
||||
"options": [
|
||||
"Олуг Мөхәммәт",
|
||||
"Әхмәт",
|
||||
"Шаһ-Әли",
|
||||
"Мамука"
|
||||
],
|
||||
"correct_answer": "Олуг Мөхәммәт",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "2013 елда Универсиада узган Казанның төп спорт комплексы ничек атала?",
|
||||
"options": [
|
||||
"Казан Арена",
|
||||
"Татнефть Арена",
|
||||
"Бөгелмә Арена",
|
||||
"Ак Барс спорт сарае"
|
||||
],
|
||||
"correct_answer": "Казан Арена",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Казандагы иң зур мәчетләрнең берсе Россиядәге кайсысы?",
|
||||
"options": [
|
||||
"Кол Шәриф мәчете",
|
||||
"Марий Эл мәчете",
|
||||
"Айша мәчете",
|
||||
"Имам Мөхәммәт мәчете"
|
||||
],
|
||||
"correct_answer": "Кол Шәриф мәчете",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Казан Кремле һәм Кол Шәриф мәчете урнашкан мәйдан ничек атала?",
|
||||
"options": [
|
||||
"Вахитов мәйданы",
|
||||
"Ирек мәйданы",
|
||||
"Кремль мәйданы",
|
||||
"Революция мәйданы"
|
||||
],
|
||||
"correct_answer": "Кремль мәйданы",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Казан гербында кайсы символ сурәтләнгән?",
|
||||
"options": [
|
||||
"Аждаһа",
|
||||
"Юлбарыс",
|
||||
"Арслан",
|
||||
"Бүре"
|
||||
],
|
||||
"correct_answer": "Аждаһа",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Казан Кремленең архитекторын атагыз.",
|
||||
"options": [
|
||||
"Иван Зарудный",
|
||||
"Фёдор Бенжамин",
|
||||
"Андрей Ушаков",
|
||||
"Юрий Дашевский"
|
||||
],
|
||||
"correct_answer": "Андрей Ушаков",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Бу югары уку йортларының кайсысы Казанда урнашкан?",
|
||||
"options": [
|
||||
"Мәскәү дәүләт университеты",
|
||||
"Казан федераль университеты",
|
||||
"Санкт-Петербург политехник университеты",
|
||||
"Новосибирск дәүләт университеты"
|
||||
],
|
||||
"correct_answer": "Казан федераль университеты",
|
||||
"image_url": "culture"
|
||||
},
|
||||
{
|
||||
"question": "Казандагы иң зур урам ничек атала? Ул шулай ук шәһәрнең төнге тормыш үзәге булып тора.",
|
||||
"options": [
|
||||
"Кремль урамы",
|
||||
"Бауман урамы",
|
||||
"Пушкин урамы",
|
||||
"Каюм Насыйри урамы"
|
||||
],
|
||||
"correct_answer": "Бауман урамы",
|
||||
"image_url": "culture"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +1,26 @@
|
||||
[{"id":1,"description":"10 слов в Data Science","imageFilename":"kart1.jpg","words":[2,3,4,5,6,7,8,9,10,11,12]}]
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"description": "10 слов в Data Science",
|
||||
"imageFilename": "kart1.jpg",
|
||||
"words": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"description": "Job Interview",
|
||||
"imageFilename": "kart1.jpg",
|
||||
"words": [13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"description": "ReactJS",
|
||||
"imageFilename": "kart1.jpg",
|
||||
"words": [23, 24, 25, 26, 27, 28, 29, 30, 31, 32]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"description": "NodeJS",
|
||||
"imageFilename": "kart1.jpg",
|
||||
"words": [33, 34, 35, 36, 37, 38, 39, 40, 41, 42]
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -607,8 +607,7 @@ function createGigachat(options = {}) {
|
||||
}
|
||||
var gigachat = createGigachat();
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
module.exports = {
|
||||
createGigachat,
|
||||
gigachat
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ router.use(async (req, res, next) => {
|
||||
process.env.GIGACHAT_ACCESS_TOKEN = json.access_token;
|
||||
process.env.GIGACHAT_EXPIRES_AT = json.expires_at;
|
||||
console.log(JSON.stringify(response.data));
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const router = require('express').Router();
|
||||
|
||||
const wordsRouter = require('./words');
|
||||
const dictionariesRouter = require('./dictionaries');
|
||||
const unitsRouter = require('./units');
|
||||
const gigachatRouter = require('./gigachat');
|
||||
const usersRouter = require('./users');
|
||||
module.exports = router;
|
||||
|
||||
const delay =
|
||||
@@ -12,6 +14,8 @@ const delay =
|
||||
};
|
||||
|
||||
router.use(delay());
|
||||
router.use('/words', wordsRouter);
|
||||
router.use('/dictionaries', dictionariesRouter);
|
||||
router.use('/units', unitsRouter);
|
||||
router.use('/gigachat', gigachatRouter);
|
||||
router.use('/gigachat', gigachatRouter);
|
||||
router.use('/users', usersRouter);
|
||||
|
||||
@@ -4,9 +4,45 @@ const router = require('express').Router();
|
||||
|
||||
module.exports = router;
|
||||
|
||||
const data = require('./data/units.json');
|
||||
const data = require('./units.json');
|
||||
router.get('/', (req, res) => {
|
||||
res.send(data);
|
||||
// for every data set author from users and save it to authoredData variable
|
||||
const users = require('../users/users.json');
|
||||
const authoredData = data.map((unit) => {
|
||||
const user = users.find((user) => user.public_id == unit.author);
|
||||
let authoredUnit = undefined;
|
||||
if (user) {
|
||||
authoredUnit = { ...unit, author: user };
|
||||
}
|
||||
return authoredUnit;
|
||||
});
|
||||
|
||||
res.send(authoredData);
|
||||
});
|
||||
|
||||
router.post('/:id', (req, res) => {
|
||||
const id = parseInt(req.params.id);
|
||||
const updatedUnit = req.body;
|
||||
|
||||
if (!updatedUnit) {
|
||||
return res.status(400).send('No unit to be added');
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return res.status(500).send('No data to be updated');
|
||||
}
|
||||
|
||||
const index = data.findIndex((unit) => unit.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
|
||||
data.splice(index, 1);
|
||||
|
||||
data.push({...updatedUnit, author: updatedUnit.author.public_id});
|
||||
|
||||
res.status(200).send(data);
|
||||
});
|
||||
|
||||
router.put('/', (req, res) => {
|
||||
@@ -16,17 +52,17 @@ router.put('/', (req, res) => {
|
||||
return res.status(400).send('No new unit to be added');
|
||||
}
|
||||
|
||||
if (!newUnit.author) {
|
||||
return res.status(400).send('User is not logged in!');
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return res.status(500).send('No data to be updated');
|
||||
}
|
||||
|
||||
const newId = data.length + 1;
|
||||
const filename = newUnit.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
fs.writeFileSync(path.join(__dirname, 'data', `${filename}.md`), newUnit.content);
|
||||
data.push({ ...newUnit, id: newId });
|
||||
|
||||
data.push({ id: newId, filename: filename, name: newUnit.name });
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, 'data', 'units.json'), JSON.stringify(data));
|
||||
res.status(200).send(data);
|
||||
});
|
||||
|
||||
@@ -39,24 +75,19 @@ router.delete('/:id', (req, res) => {
|
||||
}
|
||||
|
||||
data.splice(index, 1);
|
||||
fs.writeFileSync(path.join(__dirname, 'data', 'units.json'), JSON.stringify(data));
|
||||
res.send({ message: `Unit with ID ${id} deleted` });
|
||||
});
|
||||
|
||||
router.get('/:id', (req, res) => {
|
||||
const users = require('../users/users.json');
|
||||
const id = parseInt(req.params.id);
|
||||
const unit = data.find((unit) => unit.id === id);
|
||||
|
||||
if (!unit) {
|
||||
return res.status(404).send('Not found');
|
||||
return res.status(404).send('Unit not found');
|
||||
}
|
||||
|
||||
const unitFilepath = path.join(__dirname, 'data', `${unit.filename}.md`);
|
||||
const unitContent = fs.readFileSync(unitFilepath, 'utf-8');
|
||||
const user = users.find((user) => user.public_id == unit.author);
|
||||
|
||||
if (!unitContent) {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
|
||||
res.send({ ...unit, content: unitContent });
|
||||
res.send({...unit, author: user});
|
||||
});
|
||||
|
||||
20
server/routers/kfu-m-24-1/eng-it-lean/units/units.json
Normal file
20
server/routers/kfu-m-24-1/eng-it-lean/units/units.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"content": "# Цель урока\n\nИзучение структуры документации программы с блоком кода.\n\n## Лексика\n\n### Базовая лексика:\n\n- Documentation – документация\n- Code block – блок кода\n- Description – описание\n- Function – функция\n- Variable – переменная\n- Comment – комментарий\n\n### Расширенная лексика:\n\n- API – интерфейс прикладного программирования\n- Method – метод\n- Class – класс\n- Library – библиотека\n- Framework – фреймворк\n\n## Грамматический фокус\n\nПравило: Структура документации программы должна включать краткое описание, блок кода и примеры использования.\n\nПример:\n\nDocumentation for a program typically includes the following sections:\n\n1. **Description**: A brief overview of what the program does and its purpose.\n2. **Code Block**: The actual code that implements the functionality described in the first section.\n3. **Examples**: One or more examples demonstrating how to use the features described in the documentation.\n\nТипичные ошибки и как их избежать: Ошибки могут возникнуть из-за недостаточного описания функционала или неправильного форматирования кода. Чтобы избежать этого, важно тщательно проработать каждый раздел документации и убедиться, что все примеры корректны и понятны.\n\n## Контекстуализация\n\nТекст для анализа:\n\n**Description**: This is a simple Python script that calculates the average value of a list of numbers.\n\n**Code Block**: \n```python\ndef calculate_average(numbers):\n \"\"\"Calculate the average value of a list of numbers\"\"\"\n return sum(numbers)/len(numbers)\n```\n\nПримеры использования:\n\n```python\n# Example usage\nnumbers = [10, 20, 30]\naverage = calculate_average(numbers)\nprint(\"The average value of the list\", numbers, \"is\", average)\n```\n\n## Упражнения\n\nПисьменное задание: Написать документацию для простой функции на языке Python, которая принимает список чисел и возвращает среднее значение. Включить описание, код блока и пример использования.\n\nУстная практика: Ролевой диалог между разработчиком и техническим писателем о структуре и содержании документации программы.\n\nАналитическое задание: Проанализировать существующую документацию программы и найти ошибки или неясности. Предложить улучшения.\n\n## Домашнее задание\n\nТекстовые задачи:\n\n- Написать документацию для другой функции на языке Python, используя правильную структуру.\n- Исправить ошибки в существующей документации программы.\n- Перевести фрагмент документации на русский язык, сохраняя точность и стиль.\n",
|
||||
"id": 2,
|
||||
"author": "1738707541324",
|
||||
"name": "Документация программы"
|
||||
},
|
||||
{
|
||||
"content": "### Цель урока:\nИзучение ключевых слов и фраз, связанных с процессом трудоустройства, а также освоение базовой структуры диалога на собеседовании.\n\n### Лексика:\n**Базовая лексика:**\n1. **Job interview** – собеседование при приеме на работу\n2. **Resume / CV** – резюме\n3. **Cover letter** – сопроводительное письмо\n4. **Interviewer** – интервьюер\n5. **Application form** – анкета при приеме на работу\n6. **Salary** – зарплата\n7. **Benefits** – льготы\n\n**Расширенная лексика:**\n1. **To apply for a job** – подавать заявку на работу\n2. **To be offered the job** – получить предложение о работе\n3. **To negotiate salary** – вести переговоры о зарплате\n4. **To accept the offer** – принять предложение\n5. **To decline the offer** – отклонить предложение\n6. **To resign from your current position** – подать заявление об уходе с текущей работы\n7. **To start working at the company** – начать работать в компании\n8. **Probation period** – испытательный срок\n9. **References** – рекомендации\n10. **Work experience** – опыт работы\n\n### Грамматический фокус:\n**Правило:**\nСтруктура простого вопроса на английском языке:\n- Общий вопрос: \"Do you have any questions?\"\n- Специальный вопрос: \"What are your strengths and weaknesses?\"\n\n**Пример:**\nОбщий вопрос: \"How do you feel about this job opportunity?\"\nСпециальный вопрос: \"Can you tell me about your previous work experience?\"\n\n**Типичные ошибки и как их избежать:**\nОшибка: Неправильное использование порядка слов в вопросах.\nРешение: Практиковать построение вопросов до автоматизма.\n\n### Контекстуализация:\n**Текст для анализа:**\n\"I'm applying for the position of a marketing manager at XYZ Company. Here is my resume.\"\n\"Thank you for considering me. Can you please tell me more about the responsibilities of this role?\"\n\"Sure, let me give you an overview.\"\n\n### Упражнения:\n**Письменное задание:**\nСоставьте список из 5 вопросов, которые вы бы задали на собеседовании. Используйте простые вопросы и специальные вопросы.\n\n**Устная практика:**\nРолевая игра: один студент играет роль интервьюера, другой – кандидата на должность. Меняйтесь ролями.\n\n**Аналитическое задание:**\nНайдите и исправьте ошибки в следующем письме:\n\"Dear HR Manager,\n\nMy name is John Smith and I am writing to apply for the position of Sales Representative at ABC Inc. I enclose my resume for your review.\n\nI believe that my skills and experiences make me an ideal candidate for this position. In my current role as a sales representative at XYZ Corp, I have consistently met or exceeded my sales targets. Additionally, I possess strong communication and negotiation skills which will enable me to effectively represent your products and services.\n\nIf you would like to schedule an interview, please contact me at your convenience. Thank you for your time and consideration.\n\nBest regards,\nJohn Smith\"\n\n### Домашнее задание:\n**Текстовые задачи:**\n1. Написать сопроводительное письмо для конкретной вакансии, используя расширенную лексику.\n2. Составить резюме для воображаемой должности, включая все необходимые разделы.\n3. Перевести текст собеседования на английский язык, сохраняя структуру и смысл.",
|
||||
"id": 3,
|
||||
"author": "1738707541324",
|
||||
"name": "Job Interview"
|
||||
},
|
||||
{
|
||||
"content": "# Multifunctional Verbs\n\n## Overview\n\nThis unit focuses on the use of multifunctional verbs in English. These verbs are able to express multiple meanings depending on their use in a sentence.\n\n## Learning Objectives\n\nBy the end of this unit, you will be able to:\n\n* Identify the different forms of the main multifunctional verb.\n* Explain how these forms can be used interchangeably in sentences.\n* Demonstrate the correct usage of the three forms of the multifunctional verb by providing sentences and examples.\n\n## Vocabulary Review\n\n| Term | Definition |\n| ---- | -------------------------------------------------------- |\n| Be | To express a present or ongoing state of being. |\n| Have | To express ownership or possession. |\n| Do | To express an action to be done, future action or habit. |\n\n## Activities\n\n### Activity 1: Identify the Different Forms of the Main Multifunctional Verb\n\n* Read through each sentence and identify if the verb is used in its present tense (is), past tense (was/were), or future tense (will, would).\n* Discuss how this usage can vary depending on context.\n* Write down sentences that use different forms to illustrate your points.\n\n1. **Sentence 1**\n\n : \"The cat is sleeping.\"\n * Present tense: The cat is sleeping.\n * Past tense: The cat slept.\n * Future tense: The cat will sleep.\n2. **Sentence 2**\n\n : \"I have a dog at home.\"\n * Present tense: I have a dog.\n * Past tense: I had a dog.\n * Future tense: I will have a dog.\n3. **Sentence 3**\n\n : \"We are going on a hike tomorrow.\"\n * Present tense: We are going on a hike.\n * Past tense: We went on a hike.\n * Future tense: We will go on a hike.\n4. **Sentence 4**\n\n : \"He has been studying all day.\"\n * Present tense: He is studying.\n * Past tense: He studied.\n * Future tense: He will study.\n5. **Sentence 5**\n\n : \"We are going to buy some groceries later today.\"\n * Present tense: We are going to buy some groceries.\n * Past tense: We bought some groceries.\n * Future tense: We will buy some groceries.\n\n### Activity 2: Explain How These Forms Can Be Used Interchangeably in Sentences\n\n* Read through a sentence and identify the present, past, and future tense uses.\n* In pairs, explain why these forms are used interchangeably.\n* Provide examples of sentences that demonstrate this usage.\n* Highlight how the context changes the meaning.\n\n### Activity 3: Correct Usage of the Three Forms of the Multifunctional Verb\n\n* Read through a sentence and identify which form is being used.\n* In pairs, discuss why these forms are used in certain situations.\n* Provide sentences that demonstrate the correct usage of the three forms.",
|
||||
"id": 1,
|
||||
"author": "1738707541324",
|
||||
"name": "Multifunctional Verbs"
|
||||
}
|
||||
]
|
||||
53
server/routers/kfu-m-24-1/eng-it-lean/users/index.js
Normal file
53
server/routers/kfu-m-24-1/eng-it-lean/users/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const router = require('express').Router();
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = router;
|
||||
|
||||
let data = require('./users.json');
|
||||
const path = require('path');
|
||||
router.get('/', (req, res) => {
|
||||
res.send(data);
|
||||
});
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
const newUser = req.body;
|
||||
|
||||
data.push(newUser);
|
||||
fs.writeFileSync(path.join(__dirname, 'users.json'), JSON.stringify(data));
|
||||
res.send(data);
|
||||
});
|
||||
|
||||
router.post('/login', (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
const user = data.find((user) => user.email === email && user.password === password);
|
||||
|
||||
if (!user) {
|
||||
res.status(404).send('Пользователь не найден');
|
||||
}
|
||||
res.json({ public_id: user.public_id });
|
||||
});
|
||||
|
||||
router.get('/account', (req, res) => {
|
||||
const { public_id } = req.query;
|
||||
const user = data.find((user) => user.public_id == public_id);
|
||||
|
||||
if (!user) {
|
||||
res.status(404).send('Пользователь не найден');
|
||||
}
|
||||
res.send({ ...user, id: -1 });
|
||||
});
|
||||
|
||||
router.post('/account/save', (req, res) => {
|
||||
const updatedUser = req.body;
|
||||
const { public_id } = updatedUser;
|
||||
const index = data.findIndex((user) => user.public_id == public_id);
|
||||
|
||||
if (!index || index === -1) {
|
||||
res.status(404).send('Пользователь не найден');
|
||||
}
|
||||
|
||||
data[index] = { ...data[index], ...updatedUser, id: data[index].id, password: data[index].password };
|
||||
fs.writeFileSync(path.join(__dirname, 'users.json'), JSON.stringify(data));
|
||||
|
||||
res.status(200);
|
||||
});
|
||||
1
server/routers/kfu-m-24-1/eng-it-lean/users/users.json
Normal file
1
server/routers/kfu-m-24-1/eng-it-lean/users/users.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"id":1738707541324,"public_id":1738707541324,"email":"1@gmail.com","password":"1","age":"22","nickname":"324324","about":"Чиловый "}]
|
||||
@@ -132,5 +132,306 @@
|
||||
"During model validation, its ability to make accurate predictions on new data is checked.",
|
||||
"Validation showed that the model is robust against changes in data and has low generalization error."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"word": "resume",
|
||||
"translation": "резюме",
|
||||
"definition": "a document containing a summary of your work experience, education, and skills that you submit when applying for a job",
|
||||
"synonyms": ["CV", "curriculum vitae"],
|
||||
"examples": [
|
||||
"Make sure to update your resume before the interview."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"word": "interviewer",
|
||||
"translation": "интервьюер",
|
||||
"definition": "the person who conducts an interview, typically a representative of the company or organization offering the job",
|
||||
"synonyms": ["questioner", "examiner"],
|
||||
"examples": [
|
||||
"The interviewer asked about my previous work experiences."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"word": "qualification",
|
||||
"translation": "квалификация",
|
||||
"definition": "a quality, skill, or attribute that makes someone suitable for a particular job or activity",
|
||||
"synonyms": ["credential", "competence"],
|
||||
"examples": [
|
||||
"Do you have any qualifications in project management?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"word": "experience",
|
||||
"translation": "опыт",
|
||||
"definition": "practical contact with and observation of facts or events, especially those gained through employment",
|
||||
"synonyms": ["background", "track record"],
|
||||
"examples": [
|
||||
"She has five years of experience in marketing."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"word": "skillset",
|
||||
"translation": "набор навыков",
|
||||
"definition": "the range of skills and abilities possessed by an individual",
|
||||
"synonyms": ["abilities", "talents"],
|
||||
"examples": [
|
||||
"Her skillset includes proficiency in several programming languages."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"word": "cover letter",
|
||||
"translation": "сопроводительное письмо",
|
||||
"definition": "a document sent with your resume that provides additional information on why you're qualified for the position",
|
||||
"synonyms": ["application letter", "letter of introduction"],
|
||||
"examples": [
|
||||
"Always include a well-written cover letter with your application."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"word": "hiring manager",
|
||||
"translation": "менеджер по найму",
|
||||
"definition": "the person responsible for making hiring decisions within a company or department",
|
||||
"synonyms": ["recruiter", "HR manager"],
|
||||
"examples": [
|
||||
"The hiring manager will review all applications and select candidates for interviews."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"word": "job description",
|
||||
"translation": "описание вакансии",
|
||||
"definition": "a detailed account of the responsibilities, duties, required skills, and working conditions associated with a specific job",
|
||||
"synonyms": ["position profile", "role specification"],
|
||||
"examples": [
|
||||
"Read the job description carefully to understand what the employer is looking for."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"word": "salary negotiation",
|
||||
"translation": "переговоры о зарплате",
|
||||
"definition": "the process of discussing and agreeing upon the compensation for a job, including salary, benefits, and other forms of remuneration",
|
||||
"synonyms": ["compensation discussion", "pay bargaining"],
|
||||
"examples": [
|
||||
"It's important to prepare for salary negotiations during the final stages of the interview process."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"word": "reference",
|
||||
"translation": "рекомендация",
|
||||
"definition": "a person who can vouch for your qualifications, character, and work ethic, often contacted by potential employers",
|
||||
"synonyms": ["endorsement", "testimonial"],
|
||||
"examples": [
|
||||
"Be prepared to provide references from former supervisors or colleagues."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"word": "component",
|
||||
"translation": "компонент",
|
||||
"definition": "A reusable piece of code that renders part of the user interface.",
|
||||
"synonyms": ["module", "widget"],
|
||||
"examples": [
|
||||
"In React, components are the building blocks of the UI."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"word": "props",
|
||||
"translation": "пропсы",
|
||||
"definition": "Short for 'properties', these are read-only components passed down from parent components to child components.",
|
||||
"synonyms": ["attributes", "parameters"],
|
||||
"examples": [
|
||||
"Props allow you to pass data between components."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"word": "state",
|
||||
"translation": "состояние",
|
||||
"definition": "An object that holds data specific to a component which may change over time.",
|
||||
"synonyms": ["data", "context"],
|
||||
"examples": [
|
||||
"Managing state is crucial for dynamic web applications."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"word": "render",
|
||||
"translation": "рендеринг",
|
||||
"definition": "The process of updating the DOM to match the current state of a component.",
|
||||
"synonyms": ["update", "refresh"],
|
||||
"examples": [
|
||||
"React efficiently handles rendering to ensure smooth updates."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"word": "virtual DOM",
|
||||
"translation": "виртуальный DOM",
|
||||
"definition": "A lightweight copy of the actual DOM maintained by React to improve performance by minimizing updates to the real DOM.",
|
||||
"synonyms": ["shadow tree", "virtual representation"],
|
||||
"examples": [
|
||||
"Virtual DOM allows React to update only necessary parts of the UI."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"word": "JSX",
|
||||
"translation": "JSX",
|
||||
"definition": "A syntax extension to JavaScript used in React to describe what the user interface should look like.",
|
||||
"synonyms": ["template language", "syntax extension"],
|
||||
"examples": [
|
||||
"JSX makes it easier to write and understand React components."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"word": "hooks",
|
||||
"translation": "хуки",
|
||||
"definition": "Functions that let you use state and other React features without writing a class.",
|
||||
"synonyms": ["functionalities", "utilities"],
|
||||
"examples": [
|
||||
"Hooks make functional components more powerful."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"word": "event handling",
|
||||
"translation": "обработка событий",
|
||||
"definition": "The mechanism by which React components respond to user actions such as clicks, key presses, etc.",
|
||||
"synonyms": ["interaction management", "action response"],
|
||||
"examples": [
|
||||
"Event handlers in React allow you to define how components react to user interactions."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"word": "lifecycle methods",
|
||||
"translation": "методы жизненного цикла",
|
||||
"definition": "Methods called at different stages of a component's existence, allowing developers to perform tasks at each stage.",
|
||||
"synonyms": ["phase callbacks", "stage handlers"],
|
||||
"examples": [
|
||||
"Lifecycle methods help manage the behavior of components throughout their lifecycle."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"word": "routing",
|
||||
"translation": "маршрутизация",
|
||||
"definition": "The process of defining and managing navigation paths within a single-page application.",
|
||||
"synonyms": ["navigation control", "path management"],
|
||||
"examples": [
|
||||
"React Router is commonly used for routing in React apps."
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": 33,
|
||||
"word": "server-side",
|
||||
"translation": "серверная сторона",
|
||||
"definition": "Refers to operations performed by the server in contrast to client-side operations.",
|
||||
"synonyms": ["backend", "back-end"],
|
||||
"examples": [
|
||||
"Node.js is primarily used for server-side development."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"word": "asynchronous",
|
||||
"translation": "асинхронный",
|
||||
"definition": "Programming model where operations execute independently of other operations, allowing efficient handling of multiple requests simultaneously.",
|
||||
"synonyms": ["non-blocking", "concurrent"],
|
||||
"examples": [
|
||||
"Node.js uses asynchronous I/O to handle many concurrent connections efficiently."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"word": "callback",
|
||||
"translation": "коллбек",
|
||||
"definition": "A function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.",
|
||||
"synonyms": ["handler", "continuation"],
|
||||
"examples": [
|
||||
"Callbacks are widely used in Node.js for handling asynchronous operations."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"word": "event loop",
|
||||
"translation": "цикл событий",
|
||||
"definition": "Mechanism in Node.js that handles asynchronous operations and ensures non-blocking I/O by offloading operations to the system kernel whenever possible.",
|
||||
"synonyms": ["event-driven architecture", "runtime environment"],
|
||||
"examples": [
|
||||
"The event loop is fundamental to understanding how Node.js works."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"word": "package manager",
|
||||
"translation": "менеджер пакетов",
|
||||
"definition": "Tool used to install, update, configure, and remove packages in Node.js projects.",
|
||||
"synonyms": ["dependency manager", "library manager"],
|
||||
"examples": [
|
||||
"npm and yarn are popular package managers for Node.js."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"word": "middleware",
|
||||
"translation": "промежуточное ПО",
|
||||
"definition": "Software that sits between an application and the backend infrastructure, providing additional functionality to the request-response cycle.",
|
||||
"synonyms": ["interceptor", "filter"],
|
||||
"examples": [
|
||||
"Express.js uses middleware to handle common tasks like logging, authentication, and error handling."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"word": "REST API",
|
||||
"translation": "REST API",
|
||||
"definition": "Architectural style for building APIs using HTTP methods to perform CRUD operations on resources.",
|
||||
"synonyms": ["web service", "API design pattern"],
|
||||
"examples": [
|
||||
"Many Node.js applications implement RESTful APIs to communicate with clients."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"word": "streams",
|
||||
"translation": "потоки",
|
||||
"definition": "Data structures that facilitate continuous transfer of data in chunks rather than loading everything into memory at once.",
|
||||
"synonyms": ["data flow", "pipeline"],
|
||||
"examples": [
|
||||
"Streams are useful for handling large files or continuous data in Node.js."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"word": "cluster module",
|
||||
"translation": "модуль кластера",
|
||||
"definition": "Built-in module in Node.js that allows an application to be split into multiple processes running on separate CPU cores, improving performance and scalability.",
|
||||
"synonyms": ["multi-processing", "parallel execution"],
|
||||
"examples": [
|
||||
"Using the cluster module can significantly enhance the throughput of a Node.js application."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"word": "event emitter",
|
||||
"translation": "излучатель событий",
|
||||
"definition": "Class in Node.js that facilitates communication between objects in an application by emitting named events that other objects can listen to.",
|
||||
"synonyms": ["publisher-subscriber", "observer pattern"],
|
||||
"examples": [
|
||||
"Event emitters are useful for implementing custom event-based systems in Node.js."
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
"id": 2,
|
||||
"name": "Командная встреча",
|
||||
"description": "Ежеквартальная встреча для согласования целей",
|
||||
"date": "2025-02-02T00:00:00Z"
|
||||
"date": 1672444800000
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "День рождения",
|
||||
"description": "Празднование 30-летия Ивана",
|
||||
"date": "2025-02-02T00:00:00Z"
|
||||
"date": "2025-01-25"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
|
||||
@@ -27,10 +27,12 @@ router.post('/:user_id/:action/:id', (req, res) => {
|
||||
const user_id = parseInt(req.params.user_id);
|
||||
const id = parseInt(req.params.id);
|
||||
const action = req.params.action;
|
||||
/*
|
||||
if (users_data.findIndex((item) => item.id === user_id) === -1 || data.findIndex((item) => item.id === id) === -1) {
|
||||
res.status(404).send();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
if (action !== 'participate' && action !== 'refuse') {
|
||||
res.status(400).send({ error: 'Invalid action' });
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const axios = require('axios');
|
||||
const https = require('https');
|
||||
|
||||
process.env.GIGACHAT_AUTH =
|
||||
'YzA2ODg0NTYtYzE3Yi00OGJkLTkyY2MtMzdkM2U0YjE4ZmQ5Ojc0ZWVhN2YxLTI5MWYtNDNiZS05MTY2LWIyZjg5MGY2YWQ3Ng==';
|
||||
|
||||
const agent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
class controller {
|
||||
async getText(req, res) {
|
||||
try {
|
||||
const { text } = req.body;
|
||||
|
||||
const headers = {
|
||||
Authorization: `Basic ${process.env.GIGACHAT_AUTH}`,
|
||||
RqUID: uuidv4(),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json'
|
||||
};
|
||||
|
||||
const access_token = await axios
|
||||
.post('https://ngw.devices.sberbank.ru:9443/api/v2/oauth', encodeURI(`scope=GIGACHAT_API_PERS`), {
|
||||
headers: headers,
|
||||
httpsAgent: agent
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.access_token;
|
||||
})
|
||||
.catch((error) => {
|
||||
return error;
|
||||
});
|
||||
|
||||
const systemMessage = ` Создай анализ совместимости между двумя людьми и отобрази результат исключительно в формате JSON, который будет результатом работы 'JSON.stringify()'. Он должен состоять из следующих частей:
|
||||
|
||||
1. **Совместимость** в процентах.
|
||||
2. **Список точек соприкосновения**. Перечисли их через запятую.
|
||||
3. **Список потенциальных различий**. Перечисли через запятую.
|
||||
|
||||
Результат должен быть строкой JSON, полученной через 'JSON.stringify()', и не содержать дополнительных пояснений или текста.
|
||||
|
||||
Пример:
|
||||
{"compatibility": 70, "pointsOfContact": ["общие интересы", "совместные увлечения", "взаимное уважение"], "potentialDifferences": ["различные жизненные цели", "противоположные характеры", "несовпадающие ценности"]}
|
||||
`;
|
||||
|
||||
const textAI = await axios
|
||||
.post(
|
||||
'https://gigachat.devices.sberbank.ru/api/v1/chat/completions',
|
||||
JSON.stringify({
|
||||
model: 'GigaChat:latest',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: systemMessage + text
|
||||
}
|
||||
],
|
||||
profanity_check: true
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${access_token}`
|
||||
},
|
||||
httpsAgent: agent
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
return response.data.choices[0].message.content;
|
||||
})
|
||||
.catch((error) => {
|
||||
return error;
|
||||
});
|
||||
|
||||
res.status(200).json({ text: textAI });
|
||||
} catch (e) {
|
||||
res.status(400).json({ message: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new controller();
|
||||
@@ -0,0 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const controller = require('./controller');
|
||||
|
||||
router.post('/', controller.getText);
|
||||
|
||||
module.exports = router;
|
||||
@@ -2,8 +2,19 @@ const router = require('express').Router();
|
||||
const interestsRouter = require('./interests');
|
||||
const usersRouter = require('./users');
|
||||
const eventsRouter = require('./events');
|
||||
const gigachatRouter = require('./gigachat');
|
||||
const telegramRouter = require('./telegram');
|
||||
module.exports = router;
|
||||
|
||||
const delay =
|
||||
(ms = 1000) =>
|
||||
(req, res, next) => {
|
||||
setTimeout(next, ms);
|
||||
};
|
||||
|
||||
router.use(delay());
|
||||
router.use('/interests', interestsRouter);
|
||||
router.use('/users', usersRouter);
|
||||
router.use('/events', eventsRouter);
|
||||
router.use('/gigachat', gigachatRouter);
|
||||
router.use('/telegram', telegramRouter);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const axios = require('axios');
|
||||
|
||||
process.env.TELEGRAM_TOKEN = '7866617284:AAHDOfPQJdKmufOdRgFza6XA8ZWRHPeA_Yc';
|
||||
|
||||
class controller {
|
||||
async sendMessage(req, res) {
|
||||
try {
|
||||
const { chat_id, text } = req.body;
|
||||
|
||||
const response = await axios.get(`https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`, {
|
||||
params: {
|
||||
chat_id: chat_id,
|
||||
text: text,
|
||||
parse_mode: 'html'
|
||||
}
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (e) {
|
||||
res.status(400).json({ message: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new controller();
|
||||
@@ -0,0 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const controller = require('./controller');
|
||||
|
||||
router.post('/', controller.sendMessage);
|
||||
|
||||
module.exports = router;
|
||||
@@ -6,8 +6,19 @@
|
||||
"about": "Разработчик с 10-летним стажем, увлекаюсь новыми технологиями.",
|
||||
"email": "ivan.ivanov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Новые технологии, ИИ, техника", "label": "Новые технологии, ИИ, техника" },
|
||||
{ "value": "Музыка", "label": "Музыка" }
|
||||
"Новые технологии, ИИ, техника",
|
||||
"Музыка"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1252744945,
|
||||
"username": "Моряков Сергей",
|
||||
"photo": "",
|
||||
"about": "Люблю путешествия и фотографию, обожаю изучать новые культуры.",
|
||||
"email": "sergey.moryakov@example.com",
|
||||
"interests": [
|
||||
"Путешествия и туризм",
|
||||
"Искусство, фотография и дизайн"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -17,8 +28,8 @@
|
||||
"about": "Люблю путешествия и фотографию, обожаю изучать новые культуры.",
|
||||
"email": "maria.smirnova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Путешествия и туризм", "label": "Путешествия и туризм" },
|
||||
{ "value": "Искусство, фотография и дизайн", "label": "Искусство, фотография и дизайн" }
|
||||
"Путешествия и туризм",
|
||||
"Искусство, фотография и дизайн"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -28,8 +39,8 @@
|
||||
"about": "Финансовый аналитик, интересуюсь инвестициями и рынками.",
|
||||
"email": "aleksey.kuznetsov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Политика, социология, активизм и дебаты", "label": "Политика, социология, активизм и дебаты" },
|
||||
{ "value": "Математика, физика и информатика", "label": "Математика, физика и информатика" }
|
||||
"Политика, социология, активизм и дебаты",
|
||||
"Математика, физика и информатика"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -39,8 +50,8 @@
|
||||
"about": "Дизайнер интерьеров, люблю создавать уютные и стильные пространства.",
|
||||
"email": "olga.petrovna@example.com",
|
||||
"interests": [
|
||||
{ "value": "Искусство, фотография и дизайн", "label": "Искусство, фотография и дизайн" },
|
||||
{ "value": "Кино и другое многомодальное искусство", "label": "Кино и другое многомодальное искусство" }
|
||||
"Искусство, фотография и дизайн",
|
||||
"Кино и другое многомодальное искусство"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -50,8 +61,8 @@
|
||||
"about": "Тренер по фитнесу, придерживаюсь здорового образа жизни.",
|
||||
"email": "dmitriy.sidorov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Спорт, фитнес и ЗОЖ", "label": "Спорт, фитнес и ЗОЖ" },
|
||||
{ "value": "Волонтерство и благотворительность", "label": "Волонтерство и благотворительность" }
|
||||
"Спорт, фитнес и ЗОЖ",
|
||||
"Волонтерство и благотворительность"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -61,8 +72,8 @@
|
||||
"about": "Психолог, занимаюсь личностным ростом и развитием.",
|
||||
"email": "elena.volkova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Психология и психическое здоровье", "label": "Психология и психическое здоровье" },
|
||||
{ "value": "Литература и история", "label": "Литература и история" }
|
||||
"Психология и психическое здоровье",
|
||||
"Литература и история"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -72,8 +83,8 @@
|
||||
"about": "Ведущий мероприятий и организатор, люблю работать с людьми.",
|
||||
"email": "artem.morozov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Настольные игры", "label": "Настольные игры" },
|
||||
{ "value": "Кино и другое многомодальное искусство", "label": "Кино и другое многомодальное искусство" }
|
||||
"Настольные игры",
|
||||
"Кино и другое многомодальное искусство"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -83,8 +94,8 @@
|
||||
"about": "Веду блог о моде и стиле, увлекаюсь новыми трендами.",
|
||||
"email": "irina.fedorova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Мода", "label": "Мода" },
|
||||
{ "value": "Путешествия и туризм", "label": "Путешествия и туризм" }
|
||||
"Мода",
|
||||
"Путешествия и туризм"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -94,8 +105,8 @@
|
||||
"about": "Разработчик мобильных приложений, увлекаюсь игровыми технологиями.",
|
||||
"email": "sergey.chernov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Соревновательные видеоигры", "label": "Соревновательные видеоигры" },
|
||||
{ "value": "Новые технологии, ИИ, техника", "label": "Новые технологии, ИИ, техника" }
|
||||
"Соревновательные видеоигры",
|
||||
"Новые технологии, ИИ, техника"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -105,8 +116,8 @@
|
||||
"about": "Работаю в области маркетинга, увлекаюсь продвижением брендов.",
|
||||
"email": "tatyana.lebedeva@example.com",
|
||||
"interests": [
|
||||
{ "value": "Маркетинг", "label": "Маркетинг" },
|
||||
{ "value": "Литература и история", "label": "Литература и история" }
|
||||
"Маркетинг",
|
||||
"Литература и история"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -116,8 +127,8 @@
|
||||
"about": "Инженер-программист, увлекаюсь разработкой игр и виртуальной реальностью.",
|
||||
"email": "andrey.vasilyev@example.com",
|
||||
"interests": [
|
||||
{ "value": "Разработка игр", "label": "Разработка игр" },
|
||||
{ "value": "Виртуальная реальность", "label": "Виртуальная реальность" }
|
||||
"Разработка игр",
|
||||
"Виртуальная реальность"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -127,8 +138,8 @@
|
||||
"about": "Преподаватель литературы, люблю поэзию и классическую литературу.",
|
||||
"email": "natalya.kozlova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Литература и история", "label": "Литература и история" },
|
||||
{ "value": "Образование и наука", "label": "Образование и наука" }
|
||||
"Литература и история",
|
||||
"Образование и наука"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -138,8 +149,8 @@
|
||||
"about": "Спортсмен, занимаюсь бегом и триатлоном.",
|
||||
"email": "pavel.novikov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Спорт, фитнес и ЗОЖ", "label": "Спорт, фитнес и ЗОЖ" },
|
||||
{ "value": "Путешествия и туризм", "label": "Путешествия и туризм" }
|
||||
"Спорт, фитнес и ЗОЖ",
|
||||
"Путешествия и туризм"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -149,8 +160,8 @@
|
||||
"about": "Архитектор, увлекаюсь современным дизайном и урбанистикой.",
|
||||
"email": "ekaterina.mikhailova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Искусство, фотография и дизайн", "label": "Искусство, фотография и дизайн" },
|
||||
{ "value": "Урбанистика", "label": "Урбанистика" }
|
||||
"Искусство, фотография и дизайн",
|
||||
"Урбанистика"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -160,8 +171,8 @@
|
||||
"about": "Бизнес-консультант, помогаю компаниям развиваться.",
|
||||
"email": "viktor.sokolov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Бизнес и предпринимательство", "label": "Бизнес и предпринимательство" },
|
||||
{ "value": "Политика, социология, активизм и дебаты", "label": "Политика, социология, активизм и дебаты" }
|
||||
"Бизнес и предпринимательство",
|
||||
"Политика, социология, активизм и дебаты"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -171,8 +182,8 @@
|
||||
"about": "Художник, работаю в стиле абстракционизма.",
|
||||
"email": "anna.pavlova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Искусство, фотография и дизайн", "label": "Искусство, фотография и дизайн" },
|
||||
{ "value": "Кино и другое многомодальное искусство", "label": "Кино и другое многомодальное искусство" }
|
||||
"Искусство, фотография и дизайн",
|
||||
"Кино и другое многомодальное искусство"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -182,8 +193,8 @@
|
||||
"about": "Ученый, занимаюсь исследованиями в области биотехнологий.",
|
||||
"email": "denis.ivanov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Биология и биотехнологии", "label": "Биология и биотехнологии" },
|
||||
{ "value": "Образование и наука", "label": "Образование и наука" }
|
||||
"Биология и биотехнологии",
|
||||
"Образование и наука"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -193,8 +204,8 @@
|
||||
"about": "Повар, специализируюсь на авторской кухне.",
|
||||
"email": "lyudmila.kuznetsova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Кулинария", "label": "Кулинария" },
|
||||
{ "value": "Путешествия и туризм", "label": "Путешествия и туризм" }
|
||||
"Кулинария",
|
||||
"Путешествия и туризм"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -204,8 +215,8 @@
|
||||
"about": "Музыкант, играю на гитаре и пишу песни.",
|
||||
"email": "grigoriy.petrov@example.com",
|
||||
"interests": [
|
||||
{ "value": "Музыка", "label": "Музыка" },
|
||||
{ "value": "Кино и другое многомодальное искусство", "label": "Кино и другое многомодальное искусство" }
|
||||
"Музыка",
|
||||
"Кино и другое многомодальное искусство"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -215,19 +226,8 @@
|
||||
"about": "Врач, специализируюсь на профилактической медицине.",
|
||||
"email": "valentina.semenova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Медицина и здоровье", "label": "Медицина и здоровье" },
|
||||
{ "value": "Спорт, фитнес и ЗОЖ", "label": "Спорт, фитнес и ЗОЖ" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1252744945,
|
||||
"username": "Моряков Сергей",
|
||||
"photo": "https://i.pravatar.cc/150?img=50",
|
||||
"about": "Люблю путешествия и фотографию, обожаю изучать новые культуры.",
|
||||
"email": "maria.smirnova@example.com",
|
||||
"interests": [
|
||||
{ "value": "Путешествия и туризм", "label": "Путешествия и туризм" },
|
||||
{ "value": "Искусство, фотография и дизайн", "label": "Искусство, фотография и дизайн" }
|
||||
"Медицина и здоровье",
|
||||
"Спорт, фитнес и ЗОЖ"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -27,10 +27,12 @@ router.post('/:to_id/:action/:from_id', (req, res) => {
|
||||
const to_id = parseInt(req.params.to_id);
|
||||
const from_id = parseInt(req.params.from_id);
|
||||
const action = req.params.action;
|
||||
/*
|
||||
if (data.findIndex((item) => item.id === to_id) === -1 || data.findIndex((item) => item.id === from_id) === -1) {
|
||||
res.status(404).send();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
if (action !== 'like' && action !== 'dislike') {
|
||||
res.status(400).send({ error: 'Invalid action' });
|
||||
return;
|
||||
|
||||
421
server/routers/questioneer/index.js
Normal file
421
server/routers/questioneer/index.js
Normal file
@@ -0,0 +1,421 @@
|
||||
const express = require('express')
|
||||
const { Router } = require("express")
|
||||
const router = Router()
|
||||
const crypto = require('crypto')
|
||||
const path = require('path')
|
||||
const { getDB } = require('../../utils/mongo')
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Используем одно определение модели
|
||||
const Questionnaire = (() => {
|
||||
// Если модель уже существует, используем её
|
||||
if (mongoose.models.Questionnaire) {
|
||||
return mongoose.models.Questionnaire;
|
||||
}
|
||||
|
||||
// Иначе создаем новую модель
|
||||
const questionnaireSchema = new mongoose.Schema({
|
||||
title: { type: String, required: true },
|
||||
description: { type: String },
|
||||
questions: [{
|
||||
text: { type: String, required: true },
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['single_choice', 'multiple_choice', 'text', 'rating', 'tag_cloud', 'scale'],
|
||||
required: true
|
||||
},
|
||||
required: { type: Boolean, default: false },
|
||||
options: [{
|
||||
text: { type: String, required: true },
|
||||
count: { type: Number, default: 0 }
|
||||
}],
|
||||
scaleMin: { type: Number },
|
||||
scaleMax: { type: Number },
|
||||
scaleMinLabel: { type: String },
|
||||
scaleMaxLabel: { type: String },
|
||||
answers: [{ type: String }],
|
||||
scaleValues: [{ type: Number }],
|
||||
tags: [{
|
||||
text: { type: String },
|
||||
count: { type: Number, default: 1 }
|
||||
}]
|
||||
}],
|
||||
displayType: {
|
||||
type: String,
|
||||
enum: ['default', 'tag_cloud', 'voting', 'poll', 'step_by_step'],
|
||||
default: 'step_by_step'
|
||||
},
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
adminLink: { type: String, required: true },
|
||||
publicLink: { type: String, required: true }
|
||||
});
|
||||
|
||||
return mongoose.model('Questionnaire', questionnaireSchema);
|
||||
})();
|
||||
|
||||
// Middleware для парсинга JSON
|
||||
router.use(express.json());
|
||||
|
||||
// Обслуживание статичных файлов - проверяем правильность пути
|
||||
router.use('/static', express.static(path.join(__dirname, 'public', 'static')));
|
||||
|
||||
// Получить главную страницу
|
||||
router.get("/", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public/index.html'))
|
||||
})
|
||||
|
||||
// Страница создания нового опроса
|
||||
router.get("/create", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public/create.html'))
|
||||
})
|
||||
|
||||
// Страница редактирования опроса
|
||||
router.get("/edit/:adminLink", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public/edit.html'))
|
||||
})
|
||||
|
||||
// Страница администрирования опроса
|
||||
router.get("/admin/:adminLink", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public/admin.html'))
|
||||
})
|
||||
|
||||
// Страница голосования
|
||||
router.get("/poll/:publicLink", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public/poll.html'))
|
||||
})
|
||||
|
||||
// API для работы с опросами
|
||||
|
||||
// Создать новый опрос
|
||||
router.post("/api/questionnaires", async (req, res) => {
|
||||
try {
|
||||
// Проверка наличия нужных полей
|
||||
const { title, questions } = req.body;
|
||||
|
||||
if (!title || !Array.isArray(questions) || questions.length === 0) {
|
||||
return res.json({ success: false, error: 'Необходимо указать название и хотя бы один вопрос' });
|
||||
}
|
||||
|
||||
// Создаем уникальные ссылки
|
||||
const adminLink = crypto.randomBytes(6).toString('hex');
|
||||
const publicLink = crypto.randomBytes(6).toString('hex');
|
||||
|
||||
// Устанавливаем тип отображения step_by_step, если не указан
|
||||
if (!req.body.displayType) {
|
||||
req.body.displayType = 'step_by_step';
|
||||
}
|
||||
|
||||
// Создаем новый опросник
|
||||
const questionnaire = new Questionnaire({
|
||||
...req.body,
|
||||
adminLink,
|
||||
publicLink
|
||||
});
|
||||
|
||||
await questionnaire.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
adminLink,
|
||||
publicLink
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating questionnaire:', error);
|
||||
res.json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить все опросы
|
||||
router.get("/api/questionnaires", async (req, res) => {
|
||||
try {
|
||||
const questionnaires = await Questionnaire.find({}, {
|
||||
title: 1,
|
||||
description: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
_id: 1,
|
||||
adminLink: 1,
|
||||
publicLink: 1
|
||||
}).sort({ createdAt: -1 })
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: questionnaires
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching questionnaires:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch questionnaires'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Получить опрос по ID для админа
|
||||
router.get("/api/questionnaires/admin/:adminLink", async (req, res) => {
|
||||
try {
|
||||
const { adminLink } = req.params
|
||||
const questionnaire = await Questionnaire.findOne({ adminLink })
|
||||
|
||||
if (!questionnaire) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Questionnaire not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: questionnaire
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching questionnaire:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch questionnaire'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Получить опрос по публичной ссылке (для голосования)
|
||||
router.get("/api/questionnaires/public/:publicLink", async (req, res) => {
|
||||
try {
|
||||
const { publicLink } = req.params
|
||||
const questionnaire = await Questionnaire.findOne({ publicLink })
|
||||
|
||||
if (!questionnaire) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Questionnaire not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: questionnaire
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching questionnaire:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch questionnaire'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Обновить опрос
|
||||
router.put("/api/questionnaires/:adminLink", async (req, res) => {
|
||||
try {
|
||||
const { adminLink } = req.params
|
||||
const { title, description, questions, displayType } = req.body
|
||||
|
||||
const updatedQuestionnaire = await Questionnaire.findOneAndUpdate(
|
||||
{ adminLink },
|
||||
{
|
||||
title,
|
||||
description,
|
||||
questions,
|
||||
displayType,
|
||||
updatedAt: Date.now()
|
||||
},
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
if (!updatedQuestionnaire) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Questionnaire not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: updatedQuestionnaire
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating questionnaire:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update questionnaire'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Удалить опрос
|
||||
router.delete("/api/questionnaires/:adminLink", async (req, res) => {
|
||||
try {
|
||||
const { adminLink } = req.params
|
||||
|
||||
const deletedQuestionnaire = await Questionnaire.findOneAndDelete({ adminLink })
|
||||
|
||||
if (!deletedQuestionnaire) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Questionnaire not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Questionnaire deleted successfully'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error deleting questionnaire:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete questionnaire'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Голосование в опросе
|
||||
router.post("/api/vote/:publicLink", async (req, res) => {
|
||||
try {
|
||||
const { publicLink } = req.params
|
||||
const { answers } = req.body
|
||||
|
||||
const questionnaire = await Questionnaire.findOne({ publicLink })
|
||||
|
||||
if (!questionnaire) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Questionnaire not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Обновить счетчики голосов
|
||||
answers.forEach(answer => {
|
||||
const { questionIndex, optionIndices, textAnswer, scaleValue, tagTexts } = answer
|
||||
|
||||
// Обработка одиночного и множественного выбора
|
||||
if (Array.isArray(optionIndices)) {
|
||||
// Для множественного выбора
|
||||
optionIndices.forEach(optionIndex => {
|
||||
if (questionnaire.questions[questionIndex] &&
|
||||
questionnaire.questions[questionIndex].options[optionIndex]) {
|
||||
questionnaire.questions[questionIndex].options[optionIndex].count += 1
|
||||
}
|
||||
})
|
||||
} else if (typeof optionIndices === 'number') {
|
||||
// Для единичного выбора
|
||||
if (questionnaire.questions[questionIndex] &&
|
||||
questionnaire.questions[questionIndex].options[optionIndices]) {
|
||||
questionnaire.questions[questionIndex].options[optionIndices].count += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем текстовые ответы
|
||||
if (textAnswer && questionnaire.questions[questionIndex]) {
|
||||
if (!questionnaire.questions[questionIndex].answers) {
|
||||
questionnaire.questions[questionIndex].answers = [];
|
||||
}
|
||||
questionnaire.questions[questionIndex].answers.push(textAnswer);
|
||||
}
|
||||
|
||||
// Сохраняем ответы шкалы оценки
|
||||
if (scaleValue !== undefined && questionnaire.questions[questionIndex]) {
|
||||
if (!questionnaire.questions[questionIndex].scaleValues) {
|
||||
questionnaire.questions[questionIndex].scaleValues = [];
|
||||
}
|
||||
questionnaire.questions[questionIndex].scaleValues.push(scaleValue);
|
||||
}
|
||||
|
||||
// Сохраняем теги
|
||||
if (Array.isArray(tagTexts) && tagTexts.length > 0 && questionnaire.questions[questionIndex]) {
|
||||
if (!questionnaire.questions[questionIndex].tags) {
|
||||
questionnaire.questions[questionIndex].tags = [];
|
||||
}
|
||||
|
||||
tagTexts.forEach(tagText => {
|
||||
const existingTag = questionnaire.questions[questionIndex].tags.find(t => t.text === tagText);
|
||||
if (existingTag) {
|
||||
existingTag.count += 1;
|
||||
} else {
|
||||
questionnaire.questions[questionIndex].tags.push({ text: tagText, count: 1 });
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
await questionnaire.save()
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Vote registered successfully'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error registering vote:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to register vote'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Получить результаты опроса по публичной ссылке
|
||||
router.get("/api/results/:publicLink", async (req, res) => {
|
||||
try {
|
||||
const { publicLink } = req.params;
|
||||
const questionnaire = await Questionnaire.findOne({ publicLink });
|
||||
|
||||
if (!questionnaire) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Questionnaire not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Формируем результаты для отправки
|
||||
const results = {
|
||||
title: questionnaire.title,
|
||||
description: questionnaire.description,
|
||||
questions: questionnaire.questions.map(question => {
|
||||
const result = {
|
||||
text: question.text,
|
||||
type: question.type
|
||||
};
|
||||
|
||||
// Добавляем варианты ответов, если они есть
|
||||
if (question.options && question.options.length > 0) {
|
||||
result.options = question.options;
|
||||
}
|
||||
|
||||
// Добавляем текстовые ответы, если они есть
|
||||
if (question.answers && question.answers.length > 0) {
|
||||
result.answers = question.answers;
|
||||
}
|
||||
|
||||
// Добавляем результаты шкалы, если они есть
|
||||
if (question.scaleValues && question.scaleValues.length > 0) {
|
||||
result.scaleValues = question.scaleValues;
|
||||
|
||||
// Считаем среднее значение
|
||||
result.scaleAverage = question.scaleValues.reduce((a, b) => a + b, 0) / question.scaleValues.length;
|
||||
}
|
||||
|
||||
// Добавляем теги, если они есть
|
||||
if (question.tags && question.tags.length > 0) {
|
||||
result.tags = question.tags;
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching poll results:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch poll results'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router
|
||||
583
server/routers/questioneer/openapi.yaml
Normal file
583
server/routers/questioneer/openapi.yaml
Normal file
@@ -0,0 +1,583 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Анонимные опросы API
|
||||
description: API для работы с системой анонимных опросов
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: /questioneer/api
|
||||
description: Базовый URL API
|
||||
paths:
|
||||
/questionnaires:
|
||||
get:
|
||||
summary: Получить список опросов пользователя
|
||||
description: Возвращает список всех опросов, сохраненных в локальном хранилище браузера
|
||||
operationId: getQuestionnaires
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnairesResponse'
|
||||
post:
|
||||
summary: Создать новый опрос
|
||||
description: Создает новый опрос с указанными параметрами
|
||||
operationId: createQuestionnaire
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireCreate'
|
||||
responses:
|
||||
'200':
|
||||
description: Опрос успешно создан
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
/questionnaires/public/{publicLink}:
|
||||
get:
|
||||
summary: Получить опрос для участия
|
||||
description: Возвращает данные опроса по публичной ссылке
|
||||
operationId: getPublicQuestionnaire
|
||||
parameters:
|
||||
- name: publicLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/questionnaires/admin/{adminLink}:
|
||||
get:
|
||||
summary: Получить опрос для редактирования и просмотра результатов
|
||||
description: Возвращает данные опроса по административной ссылке
|
||||
operationId: getAdminQuestionnaire
|
||||
parameters:
|
||||
- name: adminLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
put:
|
||||
summary: Обновить опрос
|
||||
description: Обновляет существующий опрос
|
||||
operationId: updateQuestionnaire
|
||||
parameters:
|
||||
- name: adminLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireUpdate'
|
||||
responses:
|
||||
'200':
|
||||
description: Опрос успешно обновлен
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuestionnaireResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
delete:
|
||||
summary: Удалить опрос
|
||||
description: Удаляет опрос вместе со всеми ответами
|
||||
operationId: deleteQuestionnaire
|
||||
parameters:
|
||||
- name: adminLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Опрос успешно удален
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/vote/{publicLink}:
|
||||
post:
|
||||
summary: Отправить ответы на опрос
|
||||
description: Отправляет ответы пользователя на опрос
|
||||
operationId: submitVote
|
||||
parameters:
|
||||
- name: publicLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/VoteRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Ответы успешно отправлены
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/results/{publicLink}:
|
||||
get:
|
||||
summary: Получить результаты опроса
|
||||
description: Возвращает текущие результаты опроса
|
||||
operationId: getResults
|
||||
parameters:
|
||||
- name: publicLink
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Успешный запрос
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResultsResponse'
|
||||
'404':
|
||||
description: Опрос не найден
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
components:
|
||||
schemas:
|
||||
QuestionnaireCreate:
|
||||
type: object
|
||||
required:
|
||||
- title
|
||||
- questions
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов
|
||||
items:
|
||||
$ref: '#/components/schemas/Question'
|
||||
displayType:
|
||||
type: string
|
||||
description: Тип отображения опроса
|
||||
enum: [standard, step_by_step]
|
||||
default: standard
|
||||
QuestionnaireUpdate:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов
|
||||
items:
|
||||
$ref: '#/components/schemas/Question'
|
||||
displayType:
|
||||
type: string
|
||||
description: Тип отображения опроса
|
||||
enum: [standard, step_by_step]
|
||||
Question:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
- type
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст вопроса
|
||||
type:
|
||||
type: string
|
||||
description: Тип вопроса
|
||||
enum: [single, multiple, text, scale, rating, tagcloud]
|
||||
required:
|
||||
type: boolean
|
||||
description: Является ли вопрос обязательным
|
||||
default: false
|
||||
options:
|
||||
type: array
|
||||
description: Варианты ответа (для single, multiple)
|
||||
items:
|
||||
$ref: '#/components/schemas/Option'
|
||||
tags:
|
||||
type: array
|
||||
description: Список тегов (для tagcloud)
|
||||
items:
|
||||
$ref: '#/components/schemas/Tag'
|
||||
scaleMin:
|
||||
type: integer
|
||||
description: Минимальное значение шкалы (для scale)
|
||||
default: 0
|
||||
scaleMax:
|
||||
type: integer
|
||||
description: Максимальное значение шкалы (для scale)
|
||||
default: 10
|
||||
scaleMinLabel:
|
||||
type: string
|
||||
description: Метка для минимального значения шкалы
|
||||
default: "Минимум"
|
||||
scaleMaxLabel:
|
||||
type: string
|
||||
description: Метка для максимального значения шкалы
|
||||
default: "Максимум"
|
||||
Option:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст варианта ответа
|
||||
votes:
|
||||
type: integer
|
||||
description: Количество голосов за этот вариант
|
||||
default: 0
|
||||
Tag:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст тега
|
||||
count:
|
||||
type: integer
|
||||
description: Количество выборов данного тега
|
||||
default: 0
|
||||
VoteRequest:
|
||||
type: object
|
||||
required:
|
||||
- answers
|
||||
properties:
|
||||
answers:
|
||||
type: array
|
||||
description: Список ответов пользователя
|
||||
items:
|
||||
$ref: '#/components/schemas/Answer'
|
||||
Answer:
|
||||
type: object
|
||||
required:
|
||||
- questionIndex
|
||||
properties:
|
||||
questionIndex:
|
||||
type: integer
|
||||
description: Индекс вопроса
|
||||
optionIndices:
|
||||
type: array
|
||||
description: Индексы выбранных вариантов (для single, multiple)
|
||||
items:
|
||||
type: integer
|
||||
textAnswer:
|
||||
type: string
|
||||
description: Текстовый ответ пользователя (для text)
|
||||
scaleValue:
|
||||
type: integer
|
||||
description: Значение шкалы (для scale, rating)
|
||||
tagTexts:
|
||||
type: array
|
||||
description: Тексты выбранных или введенных тегов (для tagcloud)
|
||||
items:
|
||||
type: string
|
||||
QuestionnairesResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
data:
|
||||
type: array
|
||||
description: Список опросов
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionnaireInfo'
|
||||
QuestionnaireResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
data:
|
||||
$ref: '#/components/schemas/QuestionnaireData'
|
||||
QuestionnaireInfo:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
adminLink:
|
||||
type: string
|
||||
description: Административная ссылка
|
||||
publicLink:
|
||||
type: string
|
||||
description: Публичная ссылка
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата создания опроса
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата последнего обновления опроса
|
||||
QuestionnaireData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор опроса
|
||||
title:
|
||||
type: string
|
||||
description: Название опроса
|
||||
description:
|
||||
type: string
|
||||
description: Описание опроса
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionData'
|
||||
displayType:
|
||||
type: string
|
||||
description: Тип отображения опроса
|
||||
enum: [standard, step_by_step]
|
||||
adminLink:
|
||||
type: string
|
||||
description: Административная ссылка
|
||||
publicLink:
|
||||
type: string
|
||||
description: Публичная ссылка
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата создания опроса
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Дата последнего обновления опроса
|
||||
QuestionData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор вопроса
|
||||
text:
|
||||
type: string
|
||||
description: Текст вопроса
|
||||
type:
|
||||
type: string
|
||||
description: Тип вопроса
|
||||
required:
|
||||
type: boolean
|
||||
description: Является ли вопрос обязательным
|
||||
options:
|
||||
type: array
|
||||
description: Варианты ответа (для single, multiple)
|
||||
items:
|
||||
$ref: '#/components/schemas/OptionData'
|
||||
tags:
|
||||
type: array
|
||||
description: Список тегов (для tagcloud)
|
||||
items:
|
||||
$ref: '#/components/schemas/TagData'
|
||||
scaleMin:
|
||||
type: integer
|
||||
description: Минимальное значение шкалы (для scale)
|
||||
scaleMax:
|
||||
type: integer
|
||||
description: Максимальное значение шкалы (для scale)
|
||||
scaleMinLabel:
|
||||
type: string
|
||||
description: Метка для минимального значения шкалы
|
||||
scaleMaxLabel:
|
||||
type: string
|
||||
description: Метка для максимального значения шкалы
|
||||
answers:
|
||||
type: array
|
||||
description: Текстовые ответы (для text)
|
||||
items:
|
||||
type: string
|
||||
scaleValues:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
textAnswers:
|
||||
type: array
|
||||
description: Текстовые ответы (для text)
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
OptionData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор варианта ответа
|
||||
text:
|
||||
type: string
|
||||
description: Текст варианта ответа
|
||||
votes:
|
||||
type: integer
|
||||
description: Количество голосов за этот вариант
|
||||
count:
|
||||
type: integer
|
||||
description: Альтернативное поле для количества голосов
|
||||
TagData:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
type: string
|
||||
description: Идентификатор тега
|
||||
text:
|
||||
type: string
|
||||
description: Текст тега
|
||||
count:
|
||||
type: integer
|
||||
description: Количество выборов данного тега
|
||||
ResultsResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
data:
|
||||
$ref: '#/components/schemas/ResultsData'
|
||||
ResultsData:
|
||||
type: object
|
||||
properties:
|
||||
questions:
|
||||
type: array
|
||||
description: Список вопросов с результатами
|
||||
items:
|
||||
$ref: '#/components/schemas/QuestionResults'
|
||||
QuestionResults:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст вопроса
|
||||
type:
|
||||
type: string
|
||||
description: Тип вопроса
|
||||
options:
|
||||
type: array
|
||||
description: Варианты ответа с количеством голосов (для single, multiple)
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст варианта ответа
|
||||
count:
|
||||
type: integer
|
||||
description: Количество голосов
|
||||
tags:
|
||||
type: array
|
||||
description: Список тегов с количеством выборов (для tagcloud)
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
description: Текст тега
|
||||
count:
|
||||
type: integer
|
||||
description: Количество выборов
|
||||
scaleValues:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
scaleAverage:
|
||||
type: number
|
||||
description: Среднее значение шкалы (для scale, rating)
|
||||
answers:
|
||||
type: array
|
||||
description: Текстовые ответы (для text)
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
type: array
|
||||
description: Значения шкалы от пользователей (для scale, rating)
|
||||
items:
|
||||
type: integer
|
||||
SuccessResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
description: Сообщение об успешном выполнении
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Успешность запроса
|
||||
example: false
|
||||
error:
|
||||
type: string
|
||||
description: Сообщение об ошибке
|
||||
117
server/routers/questioneer/public/admin.html
Normal file
117
server/routers/questioneer/public/admin.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Управление опросом</title>
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
const basePath = window.location.pathname.split('/admin')[0];
|
||||
// Проверяем, заканчивается ли путь на слеш
|
||||
return basePath + (basePath.endsWith('/') ? 'static' : '/static');
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/admin.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигационная шапка -->
|
||||
<header class="nav-header">
|
||||
<div class="nav-container">
|
||||
<a href="javascript:;" id="nav-home-link" class="nav-logo">Анонимные опросы</a>
|
||||
<nav class="nav-menu">
|
||||
<a href="javascript:;" id="nav-main-link" class="nav-link">Главная</a>
|
||||
<a href="javascript:;" id="nav-create-link" class="nav-link">Создать опрос</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>Управление опросом</h1>
|
||||
|
||||
<div id="loading">Загрузка опроса...</div>
|
||||
|
||||
<div id="questionnaire-container" style="display: none;">
|
||||
<div class="questionnaire-header">
|
||||
<h2 id="questionnaire-title"></h2>
|
||||
<p id="questionnaire-description"></p>
|
||||
</div>
|
||||
|
||||
<div class="questionnaire-links">
|
||||
<div class="link-group">
|
||||
<h3>Ссылка для голосования:</h3>
|
||||
<div class="link-input-group">
|
||||
<input type="text" id="public-link" readonly>
|
||||
<button class="btn btn-small" id="copy-public-link">Копировать</button>
|
||||
<button class="btn btn-small" id="show-qr-code">QR-код</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="link-group">
|
||||
<h3>Административная ссылка:</h3>
|
||||
<div class="link-input-group">
|
||||
<input type="text" id="admin-link" readonly>
|
||||
<button class="btn btn-small" id="copy-admin-link">Копировать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="questionnaire-stats">
|
||||
<h3>Статистика ответов</h3>
|
||||
<div id="stats-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="questionnaire-actions">
|
||||
<button id="edit-questionnaire" class="btn">Редактировать опрос</button>
|
||||
<button id="delete-questionnaire" class="btn btn-danger">Удалить опрос</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Добавляем корректные пути к ссылкам после загрузки страницы
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Определяем базовый путь с учетом /ms в продакшен-версии
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
const basePath = isMsPath ? '/ms/questioneer' : '/questioneer';
|
||||
|
||||
// Устанавливаем правильные ссылки
|
||||
document.getElementById('nav-home-link').href = basePath;
|
||||
document.getElementById('nav-main-link').href = basePath;
|
||||
document.getElementById('nav-create-link').href = basePath + '/create';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
187
server/routers/questioneer/public/create.html
Normal file
187
server/routers/questioneer/public/create.html
Normal file
@@ -0,0 +1,187 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Создать опрос</title>
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
const basePath = window.location.pathname.split('/create')[0];
|
||||
// Проверяем, заканчивается ли путь на слеш
|
||||
return basePath + (basePath.endsWith('/') ? 'static' : '/static');
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery и остальные скрипты с учетом переменного пути -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/create.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигационная шапка -->
|
||||
<header class="nav-header">
|
||||
<div class="nav-container">
|
||||
<a href="javascript:;" id="nav-home-link" class="nav-logo">Анонимные опросы</a>
|
||||
<nav class="nav-menu">
|
||||
<a href="javascript:;" id="nav-main-link" class="nav-link">Главная</a>
|
||||
<a href="javascript:;" id="nav-create-link" class="nav-link active">Создать опрос</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>Создание нового опроса</h1>
|
||||
|
||||
<div class="form-container">
|
||||
<form id="create-questionnaire-form">
|
||||
<div class="form-group">
|
||||
<label for="title">Название опроса *</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Описание опроса</label>
|
||||
<textarea id="description" name="description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="display: none;">
|
||||
<label for="display-type">Тип отображения</label>
|
||||
<select id="display-type" name="display-type">
|
||||
<option value="step_by_step">Пошаговый</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="questions-container">
|
||||
<h2>Вопросы</h2>
|
||||
<div id="questions-list"></div>
|
||||
|
||||
<button type="button" id="add-question" class="btn btn-small">Добавить вопрос</button>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="/questioneer" class="btn btn-secondary">Отмена</a>
|
||||
<button type="submit" class="btn btn-primary">Создать опрос</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Шаблон для вопроса -->
|
||||
<template id="question-template">
|
||||
<div class="question-item" data-index="{{index}}">
|
||||
<div class="question-header">
|
||||
<h3>Вопрос {{number}}</h3>
|
||||
<button type="button" class="btn-icon delete-question">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="question-text-{{index}}">Текст вопроса *</label>
|
||||
<input type="text" id="question-text-{{index}}" class="question-text" name="questions[{{index}}][text]" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="question-type-{{index}}">Тип вопроса *</label>
|
||||
<select id="question-type-{{index}}" class="question-type-select" name="questions[{{index}}][type]" required>
|
||||
<option value="single_choice">Одиночный выбор</option>
|
||||
<option value="multiple_choice">Множественный выбор</option>
|
||||
<option value="text">Текстовый ответ</option>
|
||||
<option value="scale">Шкала оценки</option>
|
||||
<option value="tag_cloud">Облако тегов</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="questions[{{index}}][required]" value="true">
|
||||
Обязательный вопрос
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="options-container" id="options-container-{{index}}">
|
||||
<h4>Варианты ответа</h4>
|
||||
<div class="options-list" id="options-list-{{index}}"></div>
|
||||
<button type="button" class="btn btn-small add-option" data-question-index="{{index}}">Добавить вариант</button>
|
||||
</div>
|
||||
|
||||
<div class="scale-container" id="scale-container-{{index}}" style="display: none;">
|
||||
<h4>Настройки шкалы</h4>
|
||||
<div class="form-group">
|
||||
<label for="scale-min-{{index}}">Минимальное значение</label>
|
||||
<input type="number" id="scale-min-{{index}}" class="scale-min" name="questions[{{index}}][scaleMin]" value="0" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scale-max-{{index}}">Максимальное значение</label>
|
||||
<input type="number" id="scale-max-{{index}}" class="scale-max" name="questions[{{index}}][scaleMax]" value="10" min="1" max="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scale-min-label-{{index}}">Подпись минимального значения</label>
|
||||
<input type="text" id="scale-min-label-{{index}}" class="scale-min-label" name="questions[{{index}}][scaleMinLabel]" value="Минимум">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scale-max-label-{{index}}">Подпись максимального значения</label>
|
||||
<input type="text" id="scale-max-label-{{index}}" class="scale-max-label" name="questions[{{index}}][scaleMaxLabel]" value="Максимум">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Шаблон для варианта ответа -->
|
||||
<template id="option-template">
|
||||
<div class="option-item" data-index="{{optionIndex}}">
|
||||
<input type="text" name="questions[{{questionIndex}}][options][{{optionIndex}}][text]" placeholder="Вариант ответа">
|
||||
<button type="button" class="btn-icon delete-option">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Добавляем корректные пути к ссылкам после загрузки страницы
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Определяем базовый путь с учетом /ms в продакшен-версии
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
const basePath = isMsPath ? '/ms/questioneer' : '/questioneer';
|
||||
|
||||
// Устанавливаем правильные ссылки
|
||||
document.getElementById('nav-home-link').href = basePath;
|
||||
document.getElementById('nav-main-link').href = basePath;
|
||||
document.getElementById('nav-create-link').href = basePath + '/create';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
204
server/routers/questioneer/public/edit.html
Normal file
204
server/routers/questioneer/public/edit.html
Normal file
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Редактирование опроса</title>
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
const basePath = window.location.pathname.split('/edit')[0];
|
||||
// Проверяем, заканчивается ли путь на слеш
|
||||
return basePath + (basePath.endsWith('/') ? 'static' : '/static');
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/edit.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигационная шапка -->
|
||||
<header class="nav-header">
|
||||
<div class="nav-container">
|
||||
<a href="javascript:;" id="nav-home-link" class="nav-logo">Анонимные опросы</a>
|
||||
<nav class="nav-menu">
|
||||
<a href="javascript:;" id="nav-main-link" class="nav-link">Главная</a>
|
||||
<a href="javascript:;" id="nav-create-link" class="nav-link">Создать опрос</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>Редактирование опроса</h1>
|
||||
|
||||
<div id="loading">Загрузка опроса...</div>
|
||||
|
||||
<div class="form-container" id="edit-form-container" style="display: none;">
|
||||
<form id="edit-questionnaire-form">
|
||||
<div class="form-group">
|
||||
<label for="title">Название опроса:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Описание:</label>
|
||||
<textarea id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="display-type">Тип отображения:</label>
|
||||
<select id="display-type" name="displayType">
|
||||
<option value="default">Обычный</option>
|
||||
<option value="tag_cloud">Облако тегов</option>
|
||||
<option value="voting">Голосование</option>
|
||||
<option value="poll">Опрос</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="questions-container">
|
||||
<h2>Вопросы</h2>
|
||||
<div id="questions-list"></div>
|
||||
|
||||
<button type="button" id="add-question" class="btn btn-small">Добавить вопрос</button>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<div class="link-group">
|
||||
<h3>Ссылки:</h3>
|
||||
<div class="link-input-group">
|
||||
<div>
|
||||
<label for="public-link">Ссылка для голосования:</label>
|
||||
<input type="text" id="public-link" readonly>
|
||||
<button type="button" class="btn btn-small" id="copy-public-link">Копировать</button>
|
||||
<button type="button" class="btn btn-small" id="show-qr-code">QR-код</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="admin-link">Административная ссылка:</label>
|
||||
<input type="text" id="admin-link" readonly>
|
||||
<button type="button" class="btn btn-small" id="copy-admin-link">Копировать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons">
|
||||
<a href="#" id="back-to-admin" class="btn btn-secondary">Вернуться</a>
|
||||
<button type="submit" class="btn btn-primary">Сохранить изменения</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Шаблон для вопроса -->
|
||||
<template id="question-template">
|
||||
<div class="question-item" data-index="{{index}}">
|
||||
<div class="question-header">
|
||||
<h3>Вопрос {{number}}</h3>
|
||||
<button type="button" class="btn-icon delete-question">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="question-text-{{index}}">Текст вопроса:</label>
|
||||
<input type="text" id="question-text-{{index}}" name="questions[{{index}}][text]" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="question-type-{{index}}">Тип вопроса:</label>
|
||||
<select id="question-type-{{index}}" name="questions[{{index}}][type]" class="question-type-select">
|
||||
<option value="single_choice">Один вариант</option>
|
||||
<option value="multiple_choice">Несколько вариантов</option>
|
||||
<option value="text">Текстовый ответ</option>
|
||||
<option value="rating">Оценка</option>
|
||||
<option value="scale">Шкала оценки</option>
|
||||
<option value="tag_cloud">Облако тегов</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="questions[{{index}}][required]" value="true">
|
||||
Обязательный вопрос
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="options-container" id="options-container-{{index}}">
|
||||
<h4>Варианты ответа</h4>
|
||||
<div class="options-list" id="options-list-{{index}}"></div>
|
||||
<button type="button" class="btn btn-small add-option" data-question-index="{{index}}">Добавить вариант</button>
|
||||
</div>
|
||||
|
||||
<div class="scale-container" id="scale-container-{{index}}" style="display: none;">
|
||||
<h4>Настройки шкалы</h4>
|
||||
<div class="form-group">
|
||||
<label for="scale-max-{{index}}">Максимальное значение:</label>
|
||||
<select id="scale-max-{{index}}" name="questions[{{index}}][scaleMax]">
|
||||
<option value="5">5</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="20">20</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Шаблон для варианта ответа -->
|
||||
<template id="option-template">
|
||||
<div class="option-item" data-index="{{optionIndex}}">
|
||||
<input type="text" name="questions[{{questionIndex}}][options][{{optionIndex}}][text]" placeholder="Вариант ответа">
|
||||
<button type="button" class="btn-icon delete-option">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Добавляем корректные пути к ссылкам после загрузки страницы
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Определяем базовый путь с учетом /ms в продакшен-версии
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
const basePath = isMsPath ? '/ms/questioneer' : '/questioneer';
|
||||
|
||||
// Устанавливаем правильные ссылки
|
||||
document.getElementById('nav-home-link').href = basePath;
|
||||
document.getElementById('nav-main-link').href = basePath;
|
||||
document.getElementById('nav-create-link').href = basePath + '/create';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
94
server/routers/questioneer/public/index.html
Normal file
94
server/routers/questioneer/public/index.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Анонимные опросы</title>
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
const pathname = window.location.pathname;
|
||||
if (pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
// Если путь заканчивается на слеш или на /questioneer, добавляем /static
|
||||
if (pathname.endsWith('/') || pathname.endsWith('/questioneer')) {
|
||||
return pathname + '/static';
|
||||
} else {
|
||||
return pathname + '/static';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/index.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигационная шапка -->
|
||||
<header class="nav-header">
|
||||
<div class="nav-container">
|
||||
<a href="javascript:;" id="nav-home-link" class="nav-logo">Анонимные опросы</a>
|
||||
<nav class="nav-menu">
|
||||
<a href="javascript:;" id="nav-main-link" class="nav-link active">Главная</a>
|
||||
<a href="javascript:;" id="nav-create-link" class="nav-link">Создать опрос</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h1>Сервис анонимных опросов</h1>
|
||||
|
||||
<div class="main-buttons">
|
||||
<a href="javascript:;" id="create-button" class="btn">Создать новый опрос</a>
|
||||
</div>
|
||||
|
||||
<div class="questionnaires-list">
|
||||
<h2>Ваши опросы</h2>
|
||||
<div id="questionnaires-container">
|
||||
<p>Загрузка опросов...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Добавляем корректные пути к ссылкам после загрузки страницы
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Определяем базовый путь с учетом /ms в продакшен-версии
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
const basePath = isMsPath ? '/ms/questioneer' : '/questioneer';
|
||||
|
||||
// Устанавливаем правильные ссылки
|
||||
document.getElementById('nav-home-link').href = basePath;
|
||||
document.getElementById('nav-main-link').href = basePath;
|
||||
document.getElementById('nav-create-link').href = basePath + '/create';
|
||||
document.getElementById('create-button').href = basePath + '/create';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
97
server/routers/questioneer/public/poll.html
Normal file
97
server/routers/questioneer/public/poll.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Участие в опросе</title>
|
||||
<!-- Добавляем проверку на различные пути -->
|
||||
<script>
|
||||
// Определяем путь к статическим файлам с учетом prod и dev окружений
|
||||
function getStaticPath() {
|
||||
if (window.location.pathname.includes('/ms/questioneer')) {
|
||||
// Для продакшна
|
||||
return '/ms/questioneer/static';
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
const basePath = window.location.pathname.split('/poll')[0];
|
||||
// Проверяем, заканчивается ли путь на слеш
|
||||
return basePath + (basePath.endsWith('/') ? 'static' : '/static');
|
||||
}
|
||||
}
|
||||
|
||||
// Динамически добавляем CSS
|
||||
const cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = getStaticPath() + '/css/style.css';
|
||||
document.head.appendChild(cssLink);
|
||||
</script>
|
||||
|
||||
<!-- Добавляем jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Динамически добавляем скрипты
|
||||
const scriptPaths = [
|
||||
'/js/common.js',
|
||||
'/js/poll.js'
|
||||
];
|
||||
|
||||
const staticPath = getStaticPath();
|
||||
scriptPaths.forEach(path => {
|
||||
const script = document.createElement('script');
|
||||
script.src = staticPath + path;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Навигационная шапка -->
|
||||
<header class="nav-header">
|
||||
<div class="nav-container">
|
||||
<a href="javascript:;" id="nav-home-link" class="nav-logo">Анонимные опросы</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div id="loading">Загрузка опроса...</div>
|
||||
|
||||
<div id="questionnaire-container" style="display: none;">
|
||||
<div class="questionnaire-header">
|
||||
<h1 id="questionnaire-title"></h1>
|
||||
<p id="questionnaire-description"></p>
|
||||
</div>
|
||||
|
||||
<form id="poll-form">
|
||||
<div id="questions-container"></div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Отправить ответы</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="results-container" style="display: none;">
|
||||
<h2>Спасибо за участие!</h2>
|
||||
<p>Ваши ответы были успешно отправлены.</p>
|
||||
|
||||
<div class="poll-results">
|
||||
<h3>Текущие результаты:</h3>
|
||||
<div id="poll-results-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Добавляем корректные пути к ссылкам после загрузки страницы
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Определяем базовый путь с учетом /ms в продакшен-версии
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
const basePath = isMsPath ? '/ms/questioneer' : '/questioneer';
|
||||
|
||||
// Устанавливаем правильные ссылки
|
||||
document.getElementById('nav-home-link').href = basePath;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2181
server/routers/questioneer/public/static/css/style.css
Normal file
2181
server/routers/questioneer/public/static/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
465
server/routers/questioneer/public/static/js/admin.js
Normal file
465
server/routers/questioneer/public/static/js/admin.js
Normal file
@@ -0,0 +1,465 @@
|
||||
/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
|
||||
$(document).ready(function() {
|
||||
const adminLink = window.location.pathname.split('/').pop();
|
||||
let questionnaireData = null;
|
||||
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: формируем путь к API без учета текущей страницы
|
||||
// Извлекаем базовый путь из URL страницы до /admin/[adminLink]
|
||||
const basePath = pathname.split('/admin')[0];
|
||||
|
||||
// Путь до API приложения
|
||||
return basePath + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Загрузка данных опроса
|
||||
const loadQuestionnaire = () => {
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
|
||||
method: 'GET',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
questionnaireData = result.data;
|
||||
renderQuestionnaire();
|
||||
} else {
|
||||
$('#loading').text(`Ошибка: ${result.error}`);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading questionnaire:', error);
|
||||
$('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Отображение данных опроса
|
||||
const renderQuestionnaire = () => {
|
||||
// Заполняем основные данные
|
||||
$('#questionnaire-title').text(questionnaireData.title);
|
||||
$('#questionnaire-description').text(questionnaireData.description || 'Нет описания');
|
||||
|
||||
// Формируем ссылки
|
||||
const baseUrl = window.location.origin;
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
|
||||
let baseQuestionnairePath;
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
baseQuestionnairePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
baseQuestionnairePath = window.location.pathname.split('/admin')[0];
|
||||
}
|
||||
|
||||
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
|
||||
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
|
||||
|
||||
$('#public-link').val(publicUrl);
|
||||
$('#admin-link').val(adminUrl);
|
||||
|
||||
// Отображаем статистику
|
||||
renderStats(questionnaireData.questions);
|
||||
|
||||
// Показываем контейнер с данными
|
||||
$('#loading').hide();
|
||||
$('#questionnaire-container').show();
|
||||
};
|
||||
|
||||
// Отображение статистики опроса
|
||||
const renderStats = (questions) => {
|
||||
const $statsContainer = $('#stats-container');
|
||||
$statsContainer.empty();
|
||||
|
||||
// Проверяем, есть ли ответы
|
||||
let hasAnyResponses = false;
|
||||
|
||||
// Проверяем наличие ответов для каждого типа вопросов
|
||||
for (const question of questions) {
|
||||
// Согласовываем типы вопросов между бэкендом и фронтендом
|
||||
const questionType = normalizeQuestionType(question.type);
|
||||
|
||||
if (questionType === 'single' || questionType === 'multiple') {
|
||||
if (question.options && question.options.some(option => (option.votes > 0 || option.count > 0))) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (questionType === 'tagcloud') {
|
||||
if (question.tags && question.tags.some(tag => tag.count > 0)) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (questionType === 'scale' || questionType === 'rating') {
|
||||
// Проверяем оба возможных поля для данных шкалы
|
||||
const hasScaleValues = question.scaleValues && question.scaleValues.length > 0;
|
||||
const hasResponses = question.responses && question.responses.length > 0;
|
||||
if (hasScaleValues || hasResponses) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
} else if (questionType === 'text') {
|
||||
// Проверяем оба возможных поля для текстовых ответов
|
||||
const hasTextAnswers = question.textAnswers && question.textAnswers.length > 0;
|
||||
const hasAnswers = question.answers && question.answers.length > 0;
|
||||
if (hasTextAnswers || hasAnswers) {
|
||||
hasAnyResponses = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnyResponses) {
|
||||
$statsContainer.html('<div class="no-stats">Пока нет ответов на опрос</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Для каждого вопроса создаем блок статистики
|
||||
questions.forEach((question, index) => {
|
||||
const $questionStats = $('<div>', { class: 'question-stats' });
|
||||
const $questionTitle = $('<h3>', { text: `${index + 1}. ${question.text}` });
|
||||
$questionStats.append($questionTitle);
|
||||
|
||||
// Согласовываем типы вопросов между бэкендом и фронтендом
|
||||
const questionType = normalizeQuestionType(question.type);
|
||||
|
||||
// В зависимости от типа вопроса отображаем разную статистику
|
||||
if (questionType === 'single' || questionType === 'multiple') {
|
||||
// Для вопросов с выбором вариантов
|
||||
renderChoiceStats(question, $questionStats);
|
||||
} else if (questionType === 'tagcloud') {
|
||||
// Для облака тегов
|
||||
renderTagCloudStats(question, $questionStats);
|
||||
} else if (questionType === 'scale' || questionType === 'rating') {
|
||||
// Для шкалы и рейтинга
|
||||
renderScaleStats(question, $questionStats);
|
||||
} else if (questionType === 'text') {
|
||||
// Для текстовых ответов
|
||||
renderTextStats(question, $questionStats);
|
||||
}
|
||||
|
||||
$statsContainer.append($questionStats);
|
||||
});
|
||||
};
|
||||
|
||||
// Приводит тип вопроса к стандартному формату
|
||||
const normalizeQuestionType = (type) => {
|
||||
const typeMap = {
|
||||
'single_choice': 'single',
|
||||
'multiple_choice': 'multiple',
|
||||
'tag_cloud': 'tagcloud',
|
||||
'single': 'single',
|
||||
'multiple': 'multiple',
|
||||
'tagcloud': 'tagcloud',
|
||||
'scale': 'scale',
|
||||
'rating': 'rating',
|
||||
'text': 'text'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// Отображение статистики для вопросов с выбором
|
||||
const renderChoiceStats = (question, $container) => {
|
||||
// Преобразуем опции к единому формату
|
||||
const options = question.options.map(option => ({
|
||||
text: option.text,
|
||||
votes: option.votes || option.count || 0
|
||||
}));
|
||||
|
||||
const totalVotes = options.reduce((sum, option) => sum + option.votes, 0);
|
||||
|
||||
if (totalVotes === 0) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const $table = $('<table>', { class: 'stats-table' });
|
||||
const $thead = $('<thead>').append(
|
||||
$('<tr>').append(
|
||||
$('<th>', { text: 'Вариант' }),
|
||||
$('<th>', { text: 'Голоса' }),
|
||||
$('<th>', { text: '%' }),
|
||||
$('<th>', { text: 'Визуализация' })
|
||||
)
|
||||
);
|
||||
|
||||
const $tbody = $('<tbody>');
|
||||
|
||||
options.forEach(option => {
|
||||
const votes = option.votes;
|
||||
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
|
||||
|
||||
const $tr = $('<tr>').append(
|
||||
$('<td>', { text: option.text }),
|
||||
$('<td>', { text: votes }),
|
||||
$('<td>', { text: `${percent}%` }),
|
||||
$('<td>').append(
|
||||
$('<div>', { class: 'bar-container' }).append(
|
||||
$('<div>', {
|
||||
class: 'bar',
|
||||
css: { width: `${percent}%` }
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$tbody.append($tr);
|
||||
});
|
||||
|
||||
$table.append($thead, $tbody);
|
||||
$container.append($table);
|
||||
$container.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
|
||||
};
|
||||
|
||||
// Отображение статистики для облака тегов
|
||||
const renderTagCloudStats = (question, $container) => {
|
||||
if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const $tagCloud = $('<div>', { class: 'tag-cloud-stats' });
|
||||
|
||||
// Находим максимальное количество для масштабирования
|
||||
const maxCount = Math.max(...question.tags.map(tag => tag.count || 0));
|
||||
|
||||
// Сортируем теги по популярности
|
||||
const sortedTags = [...question.tags].sort((a, b) => (b.count || 0) - (a.count || 0));
|
||||
|
||||
sortedTags.forEach(tag => {
|
||||
if (tag.count && tag.count > 0) {
|
||||
const fontSize = maxCount > 0 ? 1 + (tag.count / maxCount) * 1.5 : 1; // от 1em до 2.5em
|
||||
|
||||
$tagCloud.append(
|
||||
$('<span>', {
|
||||
class: 'tag-item',
|
||||
text: `${tag.text} (${tag.count})`,
|
||||
css: { fontSize: `${fontSize}em` }
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$container.append($tagCloud);
|
||||
};
|
||||
|
||||
// Отображение статистики для шкалы и рейтинга
|
||||
const renderScaleStats = (question, $container) => {
|
||||
// Используем scaleValues или responses, в зависимости от того, что доступно
|
||||
const values = question.responses && question.responses.length > 0
|
||||
? question.responses
|
||||
: (question.scaleValues || []);
|
||||
|
||||
if (values.length === 0) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const sum = values.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / values.length;
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
|
||||
// Создаем контейнер для статистики
|
||||
const $scaleStats = $('<div>', { class: 'scale-stats' });
|
||||
|
||||
// Добавляем сводную статистику
|
||||
$scaleStats.append(
|
||||
$('<div>', { class: 'stat-summary' }).append(
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Среднее значение:' }),
|
||||
$('<span>', { class: 'stat-value', text: avg.toFixed(1) })
|
||||
),
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Минимум:' }),
|
||||
$('<span>', { class: 'stat-value', text: min })
|
||||
),
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Максимум:' }),
|
||||
$('<span>', { class: 'stat-value', text: max })
|
||||
),
|
||||
$('<div>', { class: 'stat-item' }).append(
|
||||
$('<span>', { class: 'stat-label', text: 'Количество оценок:' }),
|
||||
$('<span>', { class: 'stat-value', text: values.length })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Создаем таблицу для визуализации распределения голосов
|
||||
const $table = $('<table>', { class: 'stats-table' });
|
||||
const $thead = $('<thead>').append(
|
||||
$('<tr>').append(
|
||||
$('<th>', { text: 'Значение' }),
|
||||
$('<th>', { text: 'Голоса' }),
|
||||
$('<th>', { text: '%' }),
|
||||
$('<th>', { text: 'Визуализация' })
|
||||
)
|
||||
);
|
||||
|
||||
const $tbody = $('<tbody>');
|
||||
|
||||
// Определяем минимальное и максимальное значение шкалы из самих данных
|
||||
// либо используем значения из настроек вопроса, если они есть
|
||||
const scaleMin = question.scaleMin !== undefined ? question.scaleMin : min;
|
||||
const scaleMax = question.scaleMax !== undefined ? question.scaleMax : max;
|
||||
|
||||
// Создаем счетчик для каждого возможного значения шкалы
|
||||
const countByValue = {};
|
||||
for (let i = scaleMin; i <= scaleMax; i++) {
|
||||
countByValue[i] = 0;
|
||||
}
|
||||
|
||||
// Подсчитываем количество голосов для каждого значения
|
||||
values.forEach(value => {
|
||||
if (countByValue[value] !== undefined) {
|
||||
countByValue[value]++;
|
||||
}
|
||||
});
|
||||
|
||||
// Создаем строки таблицы для каждого значения шкалы
|
||||
for (let value = scaleMin; value <= scaleMax; value++) {
|
||||
const count = countByValue[value] || 0;
|
||||
const percent = values.length > 0 ? Math.round((count / values.length) * 100) : 0;
|
||||
|
||||
const $tr = $('<tr>').append(
|
||||
$('<td>', { text: value }),
|
||||
$('<td>', { text: count }),
|
||||
$('<td>', { text: `${percent}%` }),
|
||||
$('<td>').append(
|
||||
$('<div>', { class: 'bar-container' }).append(
|
||||
$('<div>', {
|
||||
class: 'bar',
|
||||
css: { width: `${percent}%` }
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$tbody.append($tr);
|
||||
}
|
||||
|
||||
$table.append($thead, $tbody);
|
||||
$scaleStats.append($table);
|
||||
|
||||
$container.append($scaleStats);
|
||||
};
|
||||
|
||||
// Отображение статистики для текстовых ответов
|
||||
const renderTextStats = (question, $container) => {
|
||||
// Используем textAnswers или answers, в зависимости от того, что доступно
|
||||
const answers = question.textAnswers && question.textAnswers.length > 0
|
||||
? question.textAnswers
|
||||
: (question.answers || []);
|
||||
|
||||
if (answers.length === 0) {
|
||||
$container.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const $textAnswers = $('<div>', { class: 'text-answers-list' });
|
||||
|
||||
answers.forEach((answer, i) => {
|
||||
$textAnswers.append(
|
||||
$('<div>', { class: 'text-answer-item' }).append(
|
||||
$('<div>', { class: 'answer-number', text: `#${i + 1}` }),
|
||||
$('<div>', { class: 'answer-text', text: answer })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
$container.append($textAnswers);
|
||||
};
|
||||
|
||||
// Копирование ссылок
|
||||
$('#copy-public-link').on('click', function() {
|
||||
$('#public-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование', null, true);
|
||||
});
|
||||
|
||||
$('#copy-admin-link').on('click', function() {
|
||||
$('#admin-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование', null, true);
|
||||
});
|
||||
|
||||
// Отображение QR-кода
|
||||
$('#show-qr-code').on('click', function() {
|
||||
const publicUrl = $('#public-link').val();
|
||||
showQRCodeModal(publicUrl, 'QR-код для голосования');
|
||||
});
|
||||
|
||||
// Редактирование опроса
|
||||
$('#edit-questionnaire').on('click', function() {
|
||||
// Перенаправляем на страницу редактирования
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
let basePath;
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
basePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
basePath = window.location.pathname.split('/admin')[0];
|
||||
}
|
||||
|
||||
window.location.href = `${basePath}/edit/${adminLink}`;
|
||||
});
|
||||
|
||||
// Удаление опроса
|
||||
$('#delete-questionnaire').on('click', function() {
|
||||
showConfirm('Вы уверены, что хотите удалить опрос? Все ответы будут удалены безвозвратно.', function(confirmed) {
|
||||
if (confirmed) {
|
||||
deleteQuestionnaire();
|
||||
}
|
||||
}, 'Удаление опроса');
|
||||
});
|
||||
|
||||
// Функция удаления опроса
|
||||
const deleteQuestionnaire = () => {
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/${adminLink}`,
|
||||
method: 'DELETE',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
showAlert('Опрос успешно удален', 'Удаление опроса', function() {
|
||||
// Получаем базовый путь с учетом /ms в продакшен-версии
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
let basePath;
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
basePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
basePath = window.location.pathname.split('/admin')[0];
|
||||
}
|
||||
|
||||
// Перенаправляем на главную страницу
|
||||
window.location.href = basePath;
|
||||
}, true);
|
||||
} else {
|
||||
showAlert(`Ошибка при удалении опроса: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error deleting questionnaire:', error);
|
||||
showAlert('Не удалось удалить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Инициализация
|
||||
loadQuestionnaire();
|
||||
|
||||
// Обновление данных каждые 10 секунд
|
||||
setInterval(loadQuestionnaire, 10000);
|
||||
});
|
||||
236
server/routers/questioneer/public/static/js/common.js
Normal file
236
server/routers/questioneer/public/static/js/common.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/* global $, document */
|
||||
|
||||
// Функция для создания модального окна
|
||||
function createModal(options) {
|
||||
// Если модальное окно уже существует, удаляем его
|
||||
$('.modal-overlay').remove();
|
||||
|
||||
// Опции по умолчанию
|
||||
const defaultOptions = {
|
||||
title: 'Сообщение',
|
||||
content: '',
|
||||
closeText: 'Закрыть',
|
||||
onClose: null,
|
||||
showCancel: false,
|
||||
cancelText: 'Отмена',
|
||||
confirmText: 'Подтвердить',
|
||||
onConfirm: null,
|
||||
onCancel: null,
|
||||
size: 'normal', // 'normal', 'large', 'small'
|
||||
customClass: '',
|
||||
autoClose: false, // Автоматическое закрытие по таймеру
|
||||
autoCloseTime: 2000 // Время до автоматического закрытия (2 секунды)
|
||||
};
|
||||
|
||||
// Объединяем пользовательские опции с опциями по умолчанию
|
||||
const settings = $.extend({}, defaultOptions, options);
|
||||
|
||||
// Создаем структуру модального окна
|
||||
const $modalOverlay = $('<div>', { class: 'modal-overlay' });
|
||||
const $modal = $('<div>', { class: `modal ${settings.customClass}` });
|
||||
|
||||
// Устанавливаем ширину в зависимости от размера
|
||||
if (settings.size === 'large') {
|
||||
$modal.css('max-width', '700px');
|
||||
} else if (settings.size === 'small') {
|
||||
$modal.css('max-width', '400px');
|
||||
}
|
||||
|
||||
// Создаем заголовок
|
||||
const $modalHeader = $('<div>', { class: 'modal-header' });
|
||||
const $modalTitle = $('<h3>', { text: settings.title });
|
||||
const $modalClose = $('<button>', {
|
||||
class: 'modal-close',
|
||||
html: '×',
|
||||
click: function() {
|
||||
closeModal();
|
||||
if (typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$modalHeader.append($modalTitle, $modalClose);
|
||||
|
||||
// Создаем тело
|
||||
const $modalBody = $('<div>', { class: 'modal-body' });
|
||||
if (typeof settings.content === 'string') {
|
||||
$modalBody.html(settings.content);
|
||||
} else {
|
||||
$modalBody.append(settings.content);
|
||||
}
|
||||
|
||||
// Создаем футер
|
||||
const $modalFooter = $('<div>', { class: 'modal-footer' });
|
||||
|
||||
// Если нужно показать кнопку отмены
|
||||
if (settings.showCancel) {
|
||||
const $cancelButton = $('<button>', {
|
||||
class: 'btn btn-secondary',
|
||||
text: settings.cancelText,
|
||||
click: function() {
|
||||
closeModal();
|
||||
if (typeof settings.onCancel === 'function') {
|
||||
settings.onCancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
$modalFooter.append($cancelButton);
|
||||
}
|
||||
|
||||
// Кнопка подтверждения/закрытия
|
||||
const $confirmButton = $('<button>', {
|
||||
class: settings.showCancel ? 'btn btn-primary' : 'btn',
|
||||
text: settings.showCancel ? settings.confirmText : settings.closeText,
|
||||
click: function() {
|
||||
closeModal();
|
||||
if (settings.showCancel && typeof settings.onConfirm === 'function') {
|
||||
settings.onConfirm();
|
||||
} else if (!settings.showCancel && typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
$modalFooter.append($confirmButton);
|
||||
|
||||
// Добавляем прогресс-бар, если включено автоматическое закрытие
|
||||
if (settings.autoClose) {
|
||||
const $progressBar = $('<div>', { class: 'modal-progress' });
|
||||
$modal.append($progressBar);
|
||||
}
|
||||
|
||||
// Собираем модальное окно
|
||||
$modal.append($modalHeader, $modalBody, $modalFooter);
|
||||
$modalOverlay.append($modal);
|
||||
|
||||
// Добавляем модальное окно в DOM
|
||||
$('body').append($modalOverlay);
|
||||
|
||||
// Закрытие по клику на фоне
|
||||
$modalOverlay.on('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal();
|
||||
if (typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Функция закрытия модального окна
|
||||
function closeModal() {
|
||||
$modalOverlay.removeClass('active');
|
||||
setTimeout(function() {
|
||||
$modalOverlay.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Активируем модальное окно
|
||||
setTimeout(function() {
|
||||
$modalOverlay.addClass('active');
|
||||
|
||||
// Активируем прогресс-бар и запускаем таймер закрытия, если включено автоматическое закрытие
|
||||
if (settings.autoClose) {
|
||||
const $progressBar = $modal.find('.modal-progress');
|
||||
|
||||
setTimeout(() => {
|
||||
$progressBar.addClass('active');
|
||||
}, 50);
|
||||
|
||||
setTimeout(() => {
|
||||
closeModal();
|
||||
if (typeof settings.onClose === 'function') {
|
||||
settings.onClose();
|
||||
}
|
||||
}, settings.autoCloseTime);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
// Возвращаем объект модального окна
|
||||
return {
|
||||
$modal: $modal,
|
||||
$overlay: $modalOverlay,
|
||||
close: closeModal
|
||||
};
|
||||
}
|
||||
|
||||
// Функция для отображения модального окна с сообщением (замена alert)
|
||||
function showAlert(message, title, callback, autoClose = false) {
|
||||
return createModal({
|
||||
title: title || 'Сообщение',
|
||||
content: message,
|
||||
onClose: callback,
|
||||
autoClose: autoClose,
|
||||
autoCloseTime: 2000
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для отображения модального окна с подтверждением (замена confirm)
|
||||
function showConfirm(message, callback, title) {
|
||||
return createModal({
|
||||
title: title || 'Подтверждение',
|
||||
content: message,
|
||||
showCancel: true,
|
||||
onConfirm: function() {
|
||||
if (typeof callback === 'function') {
|
||||
callback(true);
|
||||
}
|
||||
},
|
||||
onCancel: function() {
|
||||
if (typeof callback === 'function') {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для генерации QR-кода
|
||||
function generateQRCode(data, size) {
|
||||
const typeNumber = 0; // Автоматическое определение
|
||||
const errorCorrectionLevel = 'L'; // Низкий уровень коррекции ошибок
|
||||
const qr = qrcode(typeNumber, errorCorrectionLevel);
|
||||
qr.addData(data);
|
||||
qr.make();
|
||||
return qr.createImgTag(size || 8, 0);
|
||||
}
|
||||
|
||||
// Функция для отображения QR-кода в модальном окне
|
||||
function showQRCodeModal(url, title) {
|
||||
const qrCode = generateQRCode(url);
|
||||
const content = `
|
||||
<div class="qr-container">
|
||||
<div class="qr-code">
|
||||
${qrCode}
|
||||
</div>
|
||||
<div class="qr-link-container">
|
||||
<input type="text" class="qr-link-input" value="${url}" readonly>
|
||||
<button class="btn btn-copy-link">Копировать</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const modal = createModal({
|
||||
title: title || 'QR-код для доступа',
|
||||
content: content,
|
||||
size: 'large'
|
||||
});
|
||||
|
||||
// Добавляем обработчик для кнопки копирования
|
||||
modal.$modal.find('.btn-copy-link').on('click', function() {
|
||||
const input = modal.$modal.find('.qr-link-input');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
// Показываем уведомление о копировании
|
||||
const $button = $(this);
|
||||
const originalText = $button.text();
|
||||
$button.text('Скопировано!');
|
||||
$button.addClass('copied');
|
||||
|
||||
setTimeout(function() {
|
||||
$button.text(originalText);
|
||||
$button.removeClass('copied');
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
364
server/routers/questioneer/public/static/js/create.js
Normal file
364
server/routers/questioneer/public/static/js/create.js
Normal file
@@ -0,0 +1,364 @@
|
||||
/* global $, window, document, alert, showAlert, showConfirm */
|
||||
$(document).ready(function() {
|
||||
const form = $('#create-questionnaire-form');
|
||||
const questionsList = $('#questions-list');
|
||||
const addQuestionBtn = $('#add-question');
|
||||
|
||||
let questionCount = 0;
|
||||
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: формируем путь к API без учета текущей страницы
|
||||
// Извлекаем базовый путь из URL страницы до /create
|
||||
const basePath = pathname.split('/create')[0];
|
||||
|
||||
// Путь до API приложения
|
||||
return basePath + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Добавление нового вопроса
|
||||
addQuestionBtn.on('click', function() {
|
||||
addQuestion();
|
||||
});
|
||||
|
||||
// Обработка отправки формы
|
||||
form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveQuestionnaire();
|
||||
});
|
||||
|
||||
// Делегирование событий для динамических элементов
|
||||
questionsList.on('click', '.delete-question', function() {
|
||||
// Удаление вопроса
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
showConfirm('Вы уверены, что хотите удалить этот вопрос?', function(confirmed) {
|
||||
if (confirmed) {
|
||||
questionItem.remove();
|
||||
renumberQuestions();
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
questionsList.on('click', '.add-option', function() {
|
||||
// Добавление варианта ответа
|
||||
const questionIndex = $(this).data('question-index');
|
||||
addOption(questionIndex);
|
||||
});
|
||||
|
||||
questionsList.on('click', '.delete-option', function() {
|
||||
// Удаление варианта ответа
|
||||
$(this).closest('.option-item').remove();
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Делегирование для изменения типа вопроса
|
||||
questionsList.on('change', '.question-type-select', function() {
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
const questionIndex = questionItem.data('index');
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
const scaleContainer = $(`#scale-container-${questionIndex}`);
|
||||
|
||||
// Скрыть/показать варианты ответа в зависимости от типа вопроса
|
||||
const questionType = $(this).val();
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
optionsContainer.show();
|
||||
scaleContainer.hide();
|
||||
|
||||
// Если нет вариантов, добавляем два
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
if (optionsList.children().length === 0) {
|
||||
addOption(questionIndex);
|
||||
addOption(questionIndex);
|
||||
}
|
||||
|
||||
// Включаем required для полей ввода вариантов
|
||||
optionsList.find('input[type="text"]').prop('required', true);
|
||||
} else if (questionType === 'scale') {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.show();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
} else {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.hide();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Функция для добавления нового вопроса
|
||||
function addQuestion() {
|
||||
const template = $('#question-template').html();
|
||||
const index = questionCount++;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let questionHtml = template
|
||||
.replace(/\{\{index\}\}/g, index)
|
||||
.replace(/\{\{number\}\}/g, index + 1);
|
||||
|
||||
questionsList.append(questionHtml);
|
||||
|
||||
// Показываем/скрываем контейнер вариантов в зависимости от типа вопроса
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
if (!['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-container-${index}`).hide();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${index}`).find('input[type="text"]').prop('required', false);
|
||||
} else {
|
||||
// Добавляем пару начальных вариантов ответа
|
||||
addOption(index);
|
||||
addOption(index);
|
||||
}
|
||||
|
||||
if (questionType === 'scale') {
|
||||
$(`#scale-container-${index}`).show();
|
||||
} else {
|
||||
$(`#scale-container-${index}`).hide();
|
||||
}
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
}
|
||||
|
||||
// Функция для добавления варианта ответа
|
||||
function addOption(questionIndex) {
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
const template = $('#option-template').html();
|
||||
|
||||
const optionIndex = optionsList.children().length;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let optionHtml = template
|
||||
.replace(/\{\{questionIndex\}\}/g, questionIndex)
|
||||
.replace(/\{\{optionIndex\}\}/g, optionIndex);
|
||||
|
||||
optionsList.append(optionHtml);
|
||||
|
||||
// Проверяем, видим ли контейнер опций
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
if (optionsContainer.is(':hidden')) {
|
||||
// Если контейнер скрыт, отключаем required у полей ввода
|
||||
optionsList.find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
}
|
||||
|
||||
// Перенумерация вопросов
|
||||
function renumberQuestions() {
|
||||
$('.question-item').each(function(index) {
|
||||
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для обновления нумерации вопросов
|
||||
function updateQuestionNumbers() {
|
||||
$('.question-item').each(function(index) {
|
||||
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Сохранение опроса
|
||||
function saveQuestionnaire() {
|
||||
const questionnaire = {
|
||||
title: $('#title').val(),
|
||||
description: $('#description').val(),
|
||||
displayType: 'step_by_step', // Всегда устанавливаем пошаговый режим
|
||||
questions: []
|
||||
};
|
||||
|
||||
// Собираем данные о вопросах
|
||||
$('.question-item').each(function() {
|
||||
const index = $(this).data('index');
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
|
||||
const question = {
|
||||
text: $(`#question-text-${index}`).val(),
|
||||
type: questionType,
|
||||
required: $(`input[name="questions[${index}][required]"]`).is(':checked'),
|
||||
options: []
|
||||
};
|
||||
|
||||
// Добавляем настройки шкалы если нужно
|
||||
if (questionType === 'scale') {
|
||||
question.scaleMin = parseInt($(`#scale-min-${index}`).val()) || 0;
|
||||
question.scaleMax = parseInt($(`#scale-max-${index}`).val()) || 10;
|
||||
question.scaleMinLabel = $(`#scale-min-label-${index}`).val() || 'Минимум';
|
||||
question.scaleMaxLabel = $(`#scale-max-label-${index}`).val() || 'Максимум';
|
||||
}
|
||||
|
||||
// Собираем варианты ответа если это не текстовый вопрос или шкала
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-list-${index} .option-item`).each(function() {
|
||||
const optionText = $(this).find('input[type="text"]').val();
|
||||
|
||||
if (optionText) {
|
||||
question.options.push({
|
||||
text: optionText,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
questionnaire.questions.push(question);
|
||||
});
|
||||
|
||||
// Отправка на сервер
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(questionnaire),
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
// Перенаправляем на страницу администратора
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
let basePath;
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
basePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
basePath = window.location.pathname.split('/create')[0];
|
||||
}
|
||||
|
||||
window.location.href = `${basePath}/admin/${result.data.adminLink}`;
|
||||
} else {
|
||||
showAlert(`Ошибка при создании опроса: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error creating questionnaire:', error);
|
||||
showAlert('Не удалось создать опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для обновления атрибута required в зависимости от видимости полей
|
||||
function updateRequiredAttributes() {
|
||||
// Для полей вопросов
|
||||
$('.question-item').each(function() {
|
||||
const questionType = $(this).find('.question-type-select').val();
|
||||
const textInput = $(this).find('.question-text');
|
||||
const optionsContainer = $(this).find('.options-container');
|
||||
|
||||
// Обновляем required для текстового поля вопроса
|
||||
if (textInput.is(':visible')) {
|
||||
textInput.prop('required', true);
|
||||
} else {
|
||||
textInput.prop('required', false);
|
||||
}
|
||||
|
||||
// Обновляем required для полей опций
|
||||
if (questionType === 'single_choice' || questionType === 'multiple_choice') {
|
||||
optionsContainer.find('input[type="text"]').each(function() {
|
||||
if ($(this).is(':visible')) {
|
||||
$(this).prop('required', true);
|
||||
} else {
|
||||
$(this).prop('required', false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
optionsContainer.find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
|
||||
// Для шкалы оценки
|
||||
if (questionType === 'scale') {
|
||||
const minInput = $(this).find('.scale-min');
|
||||
const maxInput = $(this).find('.scale-max');
|
||||
const minLabelInput = $(this).find('.scale-min-label');
|
||||
const maxLabelInput = $(this).find('.scale-max-label');
|
||||
|
||||
if (minInput.is(':visible')) minInput.prop('required', true);
|
||||
else minInput.prop('required', false);
|
||||
|
||||
if (maxInput.is(':visible')) maxInput.prop('required', true);
|
||||
else maxInput.prop('required', false);
|
||||
|
||||
if (minLabelInput.is(':visible')) minLabelInput.prop('required', true);
|
||||
else minLabelInput.prop('required', false);
|
||||
|
||||
if (maxLabelInput.is(':visible')) maxLabelInput.prop('required', true);
|
||||
else maxLabelInput.prop('required', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Для основных полей формы
|
||||
const titleInput = $('#title');
|
||||
const descriptionInput = $('#description');
|
||||
|
||||
if (titleInput.is(':visible')) titleInput.prop('required', true);
|
||||
else titleInput.prop('required', false);
|
||||
|
||||
if (descriptionInput.is(':visible')) descriptionInput.prop('required', false); // Описание не обязательно
|
||||
}
|
||||
|
||||
// Инициализация с одним вопросом
|
||||
addQuestion();
|
||||
|
||||
// Обработчик отправки формы
|
||||
$('#create-questionnaire-form').on('submit', function(e) {
|
||||
// Обновляем атрибуты required перед отправкой
|
||||
updateRequiredAttributes();
|
||||
|
||||
// Проверяем валидность формы
|
||||
if (!this.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Находим первый невалидный элемент и прокручиваем к нему
|
||||
const firstInvalid = $(this).find(':invalid').first();
|
||||
if (firstInvalid.length) {
|
||||
$('html, body').animate({
|
||||
scrollTop: firstInvalid.offset().top - 100
|
||||
}, 500);
|
||||
|
||||
// Добавляем класс ошибки к родительскому элементу вопроса
|
||||
firstInvalid.closest('.question-item').addClass('error');
|
||||
setTimeout(() => {
|
||||
firstInvalid.closest('.question-item').removeClass('error');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
$(this).addClass('was-validated');
|
||||
});
|
||||
|
||||
// Инициализируем атрибуты required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Обработчик удаления вопроса
|
||||
$(document).on('click', '.remove-question', function() {
|
||||
$(this).closest('.question-item').remove();
|
||||
updateQuestionNumbers();
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
|
||||
// Обработчик удаления опции
|
||||
$(document).on('click', '.remove-option', function() {
|
||||
$(this).closest('.option-item').remove();
|
||||
|
||||
// Вызываем функцию обновления атрибутов required
|
||||
updateRequiredAttributes();
|
||||
});
|
||||
355
server/routers/questioneer/public/static/js/edit.js
Normal file
355
server/routers/questioneer/public/static/js/edit.js
Normal file
@@ -0,0 +1,355 @@
|
||||
/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
|
||||
$(document).ready(function() {
|
||||
const form = $('#edit-questionnaire-form');
|
||||
const questionsList = $('#questions-list');
|
||||
const addQuestionBtn = $('#add-question');
|
||||
const adminLink = window.location.pathname.split('/').pop();
|
||||
|
||||
let questionCount = 0;
|
||||
let questionnaireData = null;
|
||||
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: формируем путь к API без учета текущей страницы
|
||||
// Извлекаем базовый путь из URL страницы до /edit/[adminLink]
|
||||
const basePath = pathname.split('/edit')[0];
|
||||
|
||||
// Убеждаемся, что путь не заканчивается на /admin, если это часть URL
|
||||
const cleanPath = basePath.endsWith('/admin') ? basePath.slice(0, -6) : basePath;
|
||||
|
||||
// Путь до API приложения
|
||||
return cleanPath + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Загрузка данных опроса
|
||||
const loadQuestionnaire = () => {
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
|
||||
method: 'GET',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
questionnaireData = result.data;
|
||||
fillFormData();
|
||||
$('#loading').hide();
|
||||
$('#edit-form-container').show();
|
||||
} else {
|
||||
$('#loading').text(`Ошибка: ${result.error}`);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading questionnaire:', error);
|
||||
$('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Заполнение формы данными опроса
|
||||
const fillFormData = () => {
|
||||
// Заполняем основные данные
|
||||
$('#title').val(questionnaireData.title);
|
||||
$('#description').val(questionnaireData.description || '');
|
||||
$('#display-type').val(questionnaireData.displayType);
|
||||
|
||||
// Формируем ссылки
|
||||
const baseUrl = window.location.origin;
|
||||
const baseQuestionnairePath = window.location.pathname.split('/edit')[0];
|
||||
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
|
||||
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
|
||||
|
||||
$('#public-link').val(publicUrl);
|
||||
$('#admin-link').val(adminUrl);
|
||||
|
||||
// Добавляем вопросы
|
||||
questionsList.empty();
|
||||
|
||||
if (questionnaireData.questions && questionnaireData.questions.length > 0) {
|
||||
questionnaireData.questions.forEach((question, index) => {
|
||||
addQuestion(question);
|
||||
});
|
||||
} else {
|
||||
// Если нет вопросов, добавляем пустой
|
||||
addQuestion();
|
||||
}
|
||||
|
||||
renumberQuestions();
|
||||
};
|
||||
|
||||
// Добавление нового вопроса
|
||||
addQuestionBtn.on('click', function() {
|
||||
addQuestion();
|
||||
renumberQuestions();
|
||||
});
|
||||
|
||||
// Обработка отправки формы
|
||||
form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveQuestionnaire();
|
||||
});
|
||||
|
||||
// Делегирование событий для динамических элементов
|
||||
questionsList.on('click', '.delete-question', function() {
|
||||
// Удаление вопроса
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
questionItem.remove();
|
||||
renumberQuestions();
|
||||
});
|
||||
|
||||
questionsList.on('click', '.add-option', function() {
|
||||
// Добавление варианта ответа
|
||||
const questionIndex = $(this).data('question-index');
|
||||
addOption(questionIndex);
|
||||
});
|
||||
|
||||
questionsList.on('click', '.delete-option', function() {
|
||||
// Удаление варианта ответа
|
||||
$(this).closest('.option-item').remove();
|
||||
});
|
||||
|
||||
// Делегирование для изменения типа вопроса
|
||||
questionsList.on('change', '.question-type-select', function() {
|
||||
const questionItem = $(this).closest('.question-item');
|
||||
const questionIndex = questionItem.data('index');
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
const scaleContainer = $(`#scale-container-${questionIndex}`);
|
||||
|
||||
// Показываем/скрываем контейнеры в зависимости от типа вопроса
|
||||
const questionType = $(this).val();
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
optionsContainer.show();
|
||||
scaleContainer.hide();
|
||||
|
||||
// Если нет вариантов, добавляем два
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
if (optionsList.children().length === 0) {
|
||||
addOption(questionIndex);
|
||||
addOption(questionIndex);
|
||||
}
|
||||
|
||||
// Включаем required для полей ввода вариантов
|
||||
optionsList.find('input[type="text"]').prop('required', true);
|
||||
} else if (questionType === 'scale') {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.show();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
} else {
|
||||
optionsContainer.hide();
|
||||
scaleContainer.hide();
|
||||
// Отключаем required для скрытых полей
|
||||
$(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Копирование ссылок
|
||||
$('#copy-public-link').on('click', function() {
|
||||
$('#public-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование');
|
||||
});
|
||||
|
||||
$('#copy-admin-link').on('click', function() {
|
||||
$('#admin-link').select();
|
||||
document.execCommand('copy');
|
||||
showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование');
|
||||
});
|
||||
|
||||
// Отображение QR-кода
|
||||
$('#show-qr-code').on('click', function() {
|
||||
const publicUrl = $('#public-link').val();
|
||||
showQRCodeModal(publicUrl, 'QR-код для голосования');
|
||||
});
|
||||
|
||||
// Возврат к админке
|
||||
$('#back-to-admin').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
const basePath = window.location.pathname.split('/edit')[0];
|
||||
window.location.href = `${basePath}/admin/${adminLink}`;
|
||||
});
|
||||
|
||||
// Функция для добавления нового вопроса
|
||||
function addQuestion(questionData) {
|
||||
const template = $('#question-template').html();
|
||||
const index = questionCount++;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let questionHtml = template
|
||||
.replace(/\{\{index\}\}/g, index)
|
||||
.replace(/\{\{number\}\}/g, index + 1);
|
||||
|
||||
questionsList.append(questionHtml);
|
||||
|
||||
// Если есть данные вопроса - заполняем поля
|
||||
if (questionData) {
|
||||
$(`#question-text-${index}`).val(questionData.text);
|
||||
$(`#question-type-${index}`).val(questionData.type);
|
||||
|
||||
if (questionData.required) {
|
||||
$(`input[name="questions[${index}][required]"]`).prop('checked', true);
|
||||
}
|
||||
|
||||
// Добавляем варианты ответа если они есть
|
||||
if (questionData.options && questionData.options.length > 0) {
|
||||
questionData.options.forEach(option => {
|
||||
addOption(index, option.text);
|
||||
});
|
||||
}
|
||||
|
||||
// Заполняем настройки шкалы если нужно
|
||||
if (questionData.scaleMax) {
|
||||
$(`#scale-max-${index}`).val(questionData.scaleMax);
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем/скрываем контейнеры в зависимости от типа вопроса
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-container-${index}`).show();
|
||||
$(`#scale-container-${index}`).hide();
|
||||
|
||||
// Если нет вариантов и не загружены данные, добавляем два
|
||||
if (!questionData && $(`#options-list-${index}`).children().length === 0) {
|
||||
addOption(index);
|
||||
addOption(index);
|
||||
}
|
||||
} else if (questionType === 'scale') {
|
||||
$(`#options-container-${index}`).hide();
|
||||
$(`#scale-container-${index}`).show();
|
||||
} else {
|
||||
$(`#options-container-${index}`).hide();
|
||||
$(`#scale-container-${index}`).hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для добавления варианта ответа
|
||||
function addOption(questionIndex, optionText) {
|
||||
const optionsList = $(`#options-list-${questionIndex}`);
|
||||
const template = $('#option-template').html();
|
||||
|
||||
const optionIndex = optionsList.children().length;
|
||||
|
||||
// Заменяем плейсхолдеры в шаблоне
|
||||
let optionHtml = template
|
||||
.replace(/\{\{questionIndex\}\}/g, questionIndex)
|
||||
.replace(/\{\{optionIndex\}\}/g, optionIndex);
|
||||
|
||||
optionsList.append(optionHtml);
|
||||
|
||||
// Если есть текст варианта - устанавливаем его
|
||||
if (optionText) {
|
||||
optionsList.children().last().find('input[type="text"]').val(optionText);
|
||||
}
|
||||
|
||||
// Проверяем, видим ли контейнер опций
|
||||
const optionsContainer = $(`#options-container-${questionIndex}`);
|
||||
if (optionsContainer.is(':hidden')) {
|
||||
// Если контейнер скрыт, отключаем required у полей ввода
|
||||
optionsList.find('input[type="text"]').prop('required', false);
|
||||
}
|
||||
}
|
||||
|
||||
// Перенумерация вопросов
|
||||
function renumberQuestions() {
|
||||
$('.question-item').each(function(index) {
|
||||
$(this).find('h3').text(`Вопрос ${index + 1}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Сохранение опроса
|
||||
function saveQuestionnaire() {
|
||||
const questionnaire = {
|
||||
title: $('#title').val(),
|
||||
description: $('#description').val(),
|
||||
displayType: $('#display-type').val(),
|
||||
questions: []
|
||||
};
|
||||
|
||||
// Собираем данные о вопросах
|
||||
$('.question-item').each(function() {
|
||||
const index = $(this).data('index');
|
||||
const questionType = $(`#question-type-${index}`).val();
|
||||
|
||||
const question = {
|
||||
text: $(`#question-text-${index}`).val(),
|
||||
type: questionType,
|
||||
required: $(`input[name="questions[${index}][required]"]`).is(':checked'),
|
||||
options: []
|
||||
};
|
||||
|
||||
// Добавляем настройки шкалы если нужно
|
||||
if (questionType === 'scale') {
|
||||
question.scaleMax = parseInt($(`#scale-max-${index}`).val());
|
||||
}
|
||||
|
||||
// Собираем варианты ответа если это не текстовый вопрос или оценка
|
||||
if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
|
||||
$(`#options-list-${index} .option-item`).each(function() {
|
||||
const optionText = $(this).find('input[type="text"]').val();
|
||||
|
||||
if (optionText) {
|
||||
// Сохраняем количество голосов из старых данных
|
||||
let count = 0;
|
||||
const optionIndex = $(this).data('index');
|
||||
|
||||
if (questionnaireData &&
|
||||
questionnaireData.questions[index] &&
|
||||
questionnaireData.questions[index].options &&
|
||||
questionnaireData.questions[index].options[optionIndex]) {
|
||||
count = questionnaireData.questions[index].options[optionIndex].count || 0;
|
||||
}
|
||||
|
||||
question.options.push({
|
||||
text: optionText,
|
||||
count: count
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
questionnaire.questions.push(question);
|
||||
});
|
||||
|
||||
// Отправка на сервер
|
||||
$.ajax({
|
||||
url: `${getApiPath()}/questionnaires/${adminLink}`,
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(questionnaire),
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
showAlert('Опрос успешно сохранен!', 'Успех', { autoClose: true });
|
||||
// Перенаправляем на страницу администратора
|
||||
const isMsPath = window.location.pathname.includes('/ms/questioneer');
|
||||
let basePath;
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: используем /ms/questioneer
|
||||
basePath = '/ms/questioneer';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
basePath = window.location.pathname.split('/edit')[0];
|
||||
}
|
||||
|
||||
window.location.href = `${basePath}/admin/${adminLink}`;
|
||||
} else {
|
||||
showAlert(`Ошибка при обновлении опроса: ${result.error}`, 'Ошибка');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error updating questionnaire:', error);
|
||||
showAlert('Не удалось обновить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
loadQuestionnaire();
|
||||
});
|
||||
82
server/routers/questioneer/public/static/js/index.js
Normal file
82
server/routers/questioneer/public/static/js/index.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* global $, window, document */
|
||||
$(document).ready(function() {
|
||||
// Функция для получения базового пути API
|
||||
const getApiPath = () => {
|
||||
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
|
||||
return '/ms/questioneer/api';
|
||||
} else {
|
||||
// Для локальной разработки: формируем путь к API для главной страницы
|
||||
// Убираем завершающий слеш, если он есть
|
||||
const basePath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
||||
|
||||
// Путь до API приложения
|
||||
return basePath + '/api';
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для загрузки списка опросов
|
||||
const loadQuestionnaires = () => {
|
||||
$.ajax({
|
||||
url: getApiPath() + '/questionnaires',
|
||||
method: 'GET',
|
||||
success: function(result) {
|
||||
if (result.success) {
|
||||
renderQuestionnaires(result.data);
|
||||
} else {
|
||||
$('#questionnaires-container').html(`<p class="error">Ошибка: ${result.error}</p>`);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading questionnaires:', error);
|
||||
$('#questionnaires-container').html('<p class="error">Не удалось загрузить опросы. Пожалуйста, попробуйте позже.</p>');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Функция для отображения списка опросов
|
||||
const renderQuestionnaires = (questionnaires) => {
|
||||
if (!questionnaires || questionnaires.length === 0) {
|
||||
$('#questionnaires-container').html('<p>У вас еще нет созданных опросов.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем базовый путь (для работы и с /questioneer, и с /ms/questioneer)
|
||||
const basePath = (() => {
|
||||
const pathname = window.location.pathname;
|
||||
const isMsPath = pathname.includes('/ms/questioneer');
|
||||
|
||||
if (isMsPath) {
|
||||
// Для продакшна: нужно использовать /ms/questioneer/ для ссылок
|
||||
return '/ms/questioneer/';
|
||||
} else {
|
||||
// Для локальной разработки: используем текущий путь
|
||||
return pathname.endsWith('/') ? pathname : pathname + '/';
|
||||
}
|
||||
})();
|
||||
|
||||
const questionnairesHTML = questionnaires.map(q => `
|
||||
<div class="questionnaire-item">
|
||||
<h3>${q.title}</h3>
|
||||
<p>${q.description || 'Нет описания'}</p>
|
||||
<p>Создан: ${new Date(q.createdAt).toLocaleString()}</p>
|
||||
<div class="questionnaire-links">
|
||||
<a href="${basePath}admin/${q.adminLink}" class="btn btn-small">Редактировать</a>
|
||||
<a href="${basePath}poll/${q.publicLink}" class="btn btn-small btn-primary" target="_blank">Смотреть как участник</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
$('#questionnaires-container').html(questionnairesHTML);
|
||||
};
|
||||
|
||||
// Инициализация страницы
|
||||
loadQuestionnaires();
|
||||
|
||||
// Обновление данных каждые 30 секунд
|
||||
setInterval(loadQuestionnaires, 30000);
|
||||
});
|
||||
1202
server/routers/questioneer/public/static/js/poll.js
Normal file
1202
server/routers/questioneer/public/static/js/poll.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,11 @@ const router = Router()
|
||||
const todoRouter = require('./routes')
|
||||
const authRouter = require('./auth')
|
||||
const commentRouter = require('./comment')
|
||||
const navRouter = require('./nav')
|
||||
|
||||
router.use('/auth', authRouter)
|
||||
router.use('/comment', commentRouter)
|
||||
router.use('/nav', navRouter)
|
||||
|
||||
router.use(todoRouter)
|
||||
|
||||
|
||||
51
server/routers/todo/nav/index.js
Normal file
51
server/routers/todo/nav/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const router = require("express").Router();
|
||||
|
||||
router.get("/users", (req, res) => {
|
||||
res.send({
|
||||
success: false,
|
||||
body: [
|
||||
{
|
||||
id: "some-user-id",
|
||||
name: "alexandr",
|
||||
age: 38,
|
||||
surname: null,
|
||||
email: null,
|
||||
rated: 4,
|
||||
avatar:
|
||||
"https://www.gravatar.com/avatar/6529e885535ef67a3fad810ad71167c2c03f79480936e9b3a714731753cbb47e?d=robohash",
|
||||
friends: [
|
||||
{
|
||||
id: "2",
|
||||
name: "not alexandr",
|
||||
surname: null,
|
||||
email: null,
|
||||
rated: 2,
|
||||
avatar: "https://www.gravatar.com/avatar/6e?d=robohash",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "not alexandr",
|
||||
surname: null,
|
||||
email: null,
|
||||
age: 24,
|
||||
rated: 5,
|
||||
avatar: "https://www.gravatar.com/avatar/6e?d=robohash",
|
||||
friends: [
|
||||
{
|
||||
id: "some-user-id",
|
||||
name: "alexandr",
|
||||
surname: null,
|
||||
email: null,
|
||||
rated: 3,
|
||||
avatar:
|
||||
"https://www.gravatar.com/avatar/6529e885535ef67a3fad810ad71167c2c03f79480936e9b3a714731753cbb47e?d=robohash",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
Reference in New Issue
Block a user