diff --git a/SurveyFrontend/package-lock.json b/SurveyFrontend/package-lock.json index 432a9d5..84dc8dc 100644 --- a/SurveyFrontend/package-lock.json +++ b/SurveyFrontend/package-lock.json @@ -9,9 +9,13 @@ "version": "0.0.0", "dependencies": { "@formkit/tempo": "^0.1.2", + "chart.js": "^4.4.9", + "chartjs-plugin-annotation": "^3.1.0", + "chartjs-plugin-datalabels": "^2.2.0", "mobx": "^6.13.7", "mobx-react": "^9.2.0", "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.2", "react-textarea-autosize": "^8.5.9", @@ -972,6 +976,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1989,6 +1999,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=4.0.0" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3102,6 +3142,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", diff --git a/SurveyFrontend/package.json b/SurveyFrontend/package.json index 79c2b12..429ea89 100644 --- a/SurveyFrontend/package.json +++ b/SurveyFrontend/package.json @@ -11,9 +11,13 @@ }, "dependencies": { "@formkit/tempo": "^0.1.2", + "chart.js": "^4.4.9", + "chartjs-plugin-annotation": "^3.1.0", + "chartjs-plugin-datalabels": "^2.2.0", "mobx": "^6.13.7", "mobx-react": "^9.2.0", "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.2", "react-textarea-autosize": "^8.5.9", diff --git a/SurveyFrontend/src/assets/gmail_groups.svg b/SurveyFrontend/src/assets/gmail_groups.svg new file mode 100644 index 0000000..3e4293d --- /dev/null +++ b/SurveyFrontend/src/assets/gmail_groups.svg @@ -0,0 +1,3 @@ + + + diff --git a/SurveyFrontend/src/assets/send.svg b/SurveyFrontend/src/assets/send.svg new file mode 100644 index 0000000..6a54f6e --- /dev/null +++ b/SurveyFrontend/src/assets/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/SurveyFrontend/src/components/Results/Results.module.css b/SurveyFrontend/src/components/Results/Results.module.css index 5483bb4..e54d5f2 100644 --- a/SurveyFrontend/src/components/Results/Results.module.css +++ b/SurveyFrontend/src/components/Results/Results.module.css @@ -1,5 +1,148 @@ -/*Results.module.css*/ +/* Results.module.css */ -.results{ +.results { width: 85%; -} \ No newline at end of file + margin: 19px 30px; +} + +.statsContainer { + display: flex; + justify-content: space-between; + align-items: stretch; + margin: 30px 0; + gap: 17px; +} + +.statItem { + display: flex; + flex-direction: column; + border-radius: 15px; + min-height: 180px; + padding: 20px; + box-sizing: border-box; + position: relative; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.countAnswer { + width: 36%; + background-color: #65B953; +} + +.completion_percentage { + width: 36%; + background-color: #EEDD59; +} + +.status { + width: 24%; + background-color: #A763EB; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.statItem h3 { + margin: 0 0 15px 0; + color: #FFFFFF; + font-size: 28px; + font-weight: 600; + line-height: 1.2; +} + +.result { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-top: auto; +} + +.statItem p { + padding: 20px; + font-weight: 600; + font-size: 40px; + color: #FFFFFF; + margin: 0; + line-height: 1; +} + +.countAnswer p, +.completion_percentage p { + font-size: 60px; +} + +.imgGroup, +.imgSend { + width: 58px; + height: 61px; + align-self: flex-end; +} + +.status p { + text-align: center; + margin-top: auto; + font-size: 32px; +} + +.questionContainer { + display: flex; + flex-direction: column; + margin-bottom: 40px; + padding: 25px; + background: white; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.questionContent { + display: flex; + flex-direction: row; + gap: 40px; + align-items: flex-start; + width: 100%; +} + + +.textContainer { + display: flex; + flex-direction: column; + gap: 11px; + width: 30%; + min-width: 250px; +} + +.questionContainer h3 { + font-size: 24px; + font-weight: 600; + color: #000000; + margin: 0; +} + +.answerCount { + color: #000000; + font-size: 18px; + font-weight: 600; +} + +.chartContainer { + flex: 1; + position: relative; + display: flex; + justify-content: center; + align-items: center; +} + +.pieContainer { + width: 100%; + height: 450px; + position: relative; +} + +.barContainer { + width: 100%; + height: 450px; + display: flex; + justify-content: center; + align-items: center; + padding-right: 150px; +} diff --git a/SurveyFrontend/src/components/Results/Results.tsx b/SurveyFrontend/src/components/Results/Results.tsx index ed0e4be..24d5f1f 100644 --- a/SurveyFrontend/src/components/Results/Results.tsx +++ b/SurveyFrontend/src/components/Results/Results.tsx @@ -1,16 +1,72 @@ import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx"; -import styles from './Results.module.css' +import styles from './Results.module.css'; +import {Bar, Pie} from 'react-chartjs-2'; +import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js'; import {useOutletContext} from "react-router-dom"; import {ISurvey} from "../../api/SurveyApi.ts"; +import ChartDataLabels from 'chartjs-plugin-datalabels'; +import annotationPlugin from 'chartjs-plugin-annotation'; +import Group from '../../assets/gmail_groups.svg?react'; +import Send from '../../assets/send.svg?react'; + + +ChartJS.register( + ArcElement, Tooltip, Legend, + CategoryScale, LinearScale, BarElement, Title, ChartDataLabels, annotationPlugin +); + +// Типы для данных +interface QuestionStats { + questionText: string; + totalAnswers: number; + options: { + text: string; + percentage: number; + }[]; + isMultipleChoice?: boolean; +} export const Results = () => { - const { survey, setSurvey } = useOutletContext<{ survey: ISurvey; setSurvey: (survey: ISurvey) => void; }>(); - return( + + const surveyStats = { + totalParticipants: 100, + completionPercentage: 80, + status: 'Активен', + questions: [ + { + questionText: "Вопрос 1", + totalAnswers: 80, + options: [ + { text: "Вариант 1", percentage: 46 }, + { text: "Вариант 2", percentage: 15 }, + { text: "Вариант 3", percentage: 39 } + ], + isMultipleChoice: false + }, + { + questionText: "Вопрос 2", + totalAnswers: 100, + options: [ + { text: "Вариант 1", percentage: 50 }, + { text: "Вариант 2", percentage: 20 }, + { text: "Вариант 3", percentage: 100 }, + { text: "Вариант 4", percentage: 80 } + ], + isMultipleChoice: true + } + ] as QuestionStats[] + }; + + // Цветовая палитра + const colorsForPie = ['#67C587', '#C9EAD4', '#EAF6ED']; + const colorsForBar = ['#8979FF']; + + return (
{ setDescriptionSurvey={(value) => setSurvey({ ...survey, description: value })} setTitleSurvey={(value) => setSurvey({ ...survey, title: value })} /> +
+
+

Количество ответов

+
+

{surveyStats.totalParticipants}

+ +
+
+
+

Процент завершения

+
+

{surveyStats.completionPercentage}%

+ +
+
+
+

Статус опроса

+

{surveyStats.status}

+
+
+ + {surveyStats.questions.map((question, index) => ( +
+
+
+

{question.questionText}

+

Ответов: {question.totalAnswers}

+
+ +
+ {question.isMultipleChoice ? ( +
+ opt.text), + datasets: [{ + label: '% выбравших', + data: question.options.map(opt => opt.percentage), + backgroundColor: colorsForBar, + borderColor: colorsForBar, + borderWidth: 2, + borderRadius: 8, + borderSkipped: false, + }] + }} + options={{ + responsive: true, + plugins: { + legend: { + display: false + }, + tooltip: { enabled: true }, + datalabels: { display: false }, + annotation: { + annotations: question.options.map((opt, i) => ({ + type: 'label', + xValue: i, + yValue: opt.percentage + 5, + content: `${opt.percentage}%`, + font: { size: 16, weight: 400 }, + color: '#000' + })) + } + }, + scales: { + y: { + beginAtZero: true, + max: 100, + ticks: { callback: (val) => `${val}%` } + }, + x: { + ticks: { + color: '#000000', + font: { + size: 16, + weight: 400 + } + }, + grid: { display: false } + } + } + }} + /> +
+ ) : ( +
+ opt.text), + datasets: [{ + data: question.options.map(opt => opt.percentage), + backgroundColor: colorsForPie, + borderColor: '#fff', + borderWidth: 2 + }] + }} + options={{ + responsive: true, + plugins: { + legend: { + position: 'right', + labels: { + color: '#000000', + font: { + size: 18, + weight: 500 + } + } + }, + tooltip: { + callbacks: { + label: (ctx) => `${ctx.label}: ${ctx.raw}%` + } + }, + datalabels: { + formatter: (value) => `${value}%`, + color: '#000', + font: { weight: 400, size: 16 } + } + }, + animation: { animateRotate: true } + }} + /> +
+ )} +
+
+
+ ))}
- ) -} \ No newline at end of file + ); +}; \ No newline at end of file