Compare commits

..

5 Commits

Author SHA1 Message Date
MikanD
9c6272db25 Profession page init [AI] 2025-06-01 16:48:44 +03:00
MikanD
83af205e7e survey result page 2025-06-01 16:17:09 +03:00
MikanD
4a5b0a9532 add result page 2025-06-01 00:52:20 +03:00
MikanD
48ec88df74 SurveyPage [AI] 2025-05-31 22:12:25 +03:00
MikanD
7555757ca3 pre AI 2025-05-31 18:15:52 +03:00
10 changed files with 1426 additions and 56 deletions

1107
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.6.0"
},
"devDependencies": {

View File

@@ -57,7 +57,7 @@ const createQueryString = (params: Record<string, string | number | boolean | un
return filteredParams.join('&');
};
export function useDataPage<T>(application:string, endpoint: string, params:{[key:string]:string|number}={}) {
export function useDataPage<T>(application:string, endpoint: string, params:{[key:string]:string|number}={}, needLoad:boolean=true):PageControl<T> {
const [data, setData] = useState<T[]|null>(null);
// const [previousUrl, setPreviousUrl] = useState<string|undefined>(undefined)
// const [nextUrl, setNextUrl] = useState<string|undefined>(undefined)
@@ -67,7 +67,7 @@ export function useDataPage<T>(application:string, endpoint: string, params:{[ke
useEffect(
()=>{
if (data === null){
if (data === null && needLoad){
getFetch(`/${application}/${endpoint}/?${createQueryString(params)}`).then((r)=>{
if (r.success){
const pageData = r.body as PageResponse<T>;
@@ -82,7 +82,7 @@ export function useDataPage<T>(application:string, endpoint: string, params:{[ke
})
}
},
[data, application, endpoint]
[data, application, endpoint, needLoad]
)
// function changePage(pageUrl: string|undefined) {
@@ -110,8 +110,8 @@ export function useDataPage<T>(application:string, endpoint: string, params:{[ke
return {
items: data,
hasPrevious: undefined,//previousUrl !== undefined,
hasNext: undefined,//nextUrl !== undefined,
hasPrevious: false,//previousUrl !== undefined,
hasNext: false,//nextUrl !== undefined,
nextPage,
previousPage
} as PageControl<T>
@@ -119,25 +119,25 @@ export function useDataPage<T>(application:string, endpoint: string, params:{[ke
}
export function useData<T>(application:string, endpoint: string, id: string|number) {
export function useData<T>(application:string, endpoint: string, id: string|number, needLoad:boolean=true): T|null|undefined {
const [data, setData] = useState<T | null | undefined>(null);
useEffect(
()=>{
if (data === null){
if (data === null && needLoad){
getFetch(`/${application}/${endpoint}/${id}`).then((r)=>{
if (r.success){
const pageData = r.body as T;
setData(pageData);
}
else {
setData(null);
setData(undefined);
}
})
}
},
[data, application, endpoint, id]
[data, application, endpoint, id, needLoad]
)
return data;

View File

@@ -1 +0,0 @@

View File

@@ -1,9 +1,10 @@
// App.tsx
import {BrowserRouter as Router, Routes, Route, Navigate, Link} from 'react-router-dom';
import React from "react";
import {MainPage} from "./pages/MainPage.tsx";
import {SurveyPage} from "./pages/SurveyPage.tsx";
import {DashboardPage} from "./pages/DashboardPage.tsx";
import { SurveyResultPage } from './pages/SurveyResultPage.tsx';
import { ProfessionPage } from './pages/ProfessionPage.tsx';
@@ -16,9 +17,11 @@ export default function App() {
</div>
<Routes>
<Route path="/" element={<MainPage/>}/>
<Route path="/surveys/results/:scoreVariableId/" element={<SurveyResultPage/>}/>
<Route path="/surveys/professions/:professionId/" element={<ProfessionPage/>}/>
<Route path="/surveys/:surveyId/" element={<SurveyPage/>}/>
<Route path="/dashboard/" element={<DashboardPage/>}/>
<Route path="*" element={<Navigate to="/"/>}/>
{/* <Route path="*" element={<Navigate to="/"/>}/> */}
</Routes>
</Router>
);

View File

@@ -1,7 +1,7 @@
import {ReactElement} from "react";
import {Link} from "react-router-dom";
export function LoadingData({data, children}: {data: object|null|undefined, children: ReactElement}) {
export function LoadingData({data, children}: {data: object|null|undefined, children: (ReactElement | null | undefined | boolean)[]}) {
if (data === null){
return <div className={"loading"}>Идёт загрузка...</div>

View File

@@ -0,0 +1,78 @@
import { useParams } from 'react-router-dom';
import { useData, useDataPage } from '../API/common';
import { Institution, Profession, Specialty } from '../types/survey';
import ReactMarkdown from 'react-markdown';
import { LoadingData } from '../components/LoadingData';
import { useEffect, useMemo, useState } from 'react';
import { LoadingList } from '../components/LoadingList';
export function ProfessionPage() {
const { professionId } = useParams<{ professionId: string }>();
const profession = useData<Profession>('surveys', 'professions', professionId || 0);
const specialties = useDataPage<Specialty>('surveys', 'specialties', { profession: professionId || 0 }, !!profession);
const [institutionCache, setInstitutionCache] = useState<Map<number, Institution>>(new Map()); // Кеширование учебных заведений
const institutionsToLoad = useMemo(() => {
if (!specialties.items) return [];
return specialties.items
.map(specialty => specialty.institution)
.filter(id => !institutionCache.has(id));
}, [specialties.items, institutionCache]);
useEffect(() => {
if (institutionsToLoad.length > 0) {
Promise.all(
institutionsToLoad.map(id =>
useData<Institution>('surveys', 'institutions', id, true)
)
).then(newInstitutions => {
setInstitutionCache(prev => {
const newCache = new Map(prev);
newInstitutions.forEach(institution => institution && newCache.set(institution.id, institution));
return newCache;
});
});
}
}, [institutionsToLoad]);
return (
<div>
<LoadingData data={profession}>
<h1>{profession?.name}</h1>
<div className="description">
<ReactMarkdown>{profession?.description || ''}</ReactMarkdown>
</div>
<div className="specialties-block">
<h2>Где можно обучиться</h2>
<LoadingList
data={specialties.items}
listElement={(specialty) => (
<SpecialtyBlock
key={specialty.id}
specialty={specialty as Specialty}
institution={institutionCache.get((specialty as Specialty).institution)}
/>
)}
/>
</div>
</LoadingData>
</div>
);
}
function SpecialtyBlock({
specialty,
institution,
}: {
specialty: Specialty;
institution: Institution | undefined;
}) {
return (
<div className="specialty-block">
<h3>{specialty.name}</h3>
<p>Учебное заведение: {institution?.name || 'Загрузка...'}</p>
</div>
);
}

View File

@@ -1,53 +1,165 @@
import {LoadingList} from "../components/LoadingList.tsx";
import {Survey} from "../types/survey.ts";
import {Link, useParams} from "react-router-dom";
import React from "react";
import {useData, useDataPage} from "../API/common.ts";
import {LoadingData} from "../components/LoadingData.tsx";
import React, { useState, useEffect } from "react";
import { Navigate, useNavigate, useParams } from "react-router-dom";
import {
Survey,
Question,
QuestionPair,
AgreementQuestion,
QuestionType,
ScoreVariable,
} from "../types/survey";
import { useData, useDataPage } from "../API/common";
import { LoadingData } from "../components/LoadingData";
import { LoadingList } from "../components/LoadingList";
export function SurveyPage() {
const { surveyId } = useParams<{ surveyId: string }>();
const navigate = useNavigate();
const {surveyId} = useParams();
// Загрузка теста
const survey = useData<Survey>("surveys", "surveys", surveyId || 0);
const survey = useData<Survey>("surveys", "surveys", surveyId);
// Загрузка вопросов
const [questions, setQuestions] = useState<Question[] | null>(null);
const questionsPairs = useDataPage<QuestionPair>("surveys", "question-pairs", { survey: surveyId || 0 });
const questionsAgreement = useDataPage<AgreementQuestion>("surveys", "agreement-questions", { survey: surveyId || 0 });
// Состояние для ответов
const [answers, setAnswers] = useState<Record<string, number>>({});
const [isCompleted, setIsCompleted] = useState(false);
// Объединение вопросов после загрузки
useEffect(() => {
if (
questions === null &&
questionsPairs.items !== null &&
questionsAgreement.items !== null
) {
const mergedQuestions = [
...questionsPairs.items.map((q:Question) => ({ ...q, questionType: QuestionType.QuestionPair })),
...questionsAgreement.items.map((q:Question) => ({ ...q, questionType: QuestionType.AgreementQuestion })),
];
mergedQuestions.sort(() => Math.random() - 0.5); // Перемешивание
setQuestions(mergedQuestions);
}
}, [questions, questionsPairs.items, questionsAgreement.items]);
// Проверка завершения теста
useEffect(() => {
if (questions && Object.keys(answers).length === questions.length) {
setIsCompleted(true);
} else {
setIsCompleted(false);
}
}, [questions, answers]);
// Обработка ответов
const handleAnswer = (questionKey: string, answerValue: number) => {
setAnswers((prev) => ({ ...prev, [questionKey]: answerValue }));
};
// Вычисление итогового score_variable
const calculateResults = () => {
if (!questions) return;
const scoreCounts: Record<string, number> = {};
questions.forEach((question) => {
const questionKey = `${question.questionType}_${question.id}`;
const answer = answers[questionKey];
if (answer !== undefined) {
if (question.questionType === QuestionType.QuestionPair) {
const selectedStatement = (question as QuestionPair).statements.find(
(s) => s.id === answer
);
if (selectedStatement) {
scoreCounts[selectedStatement.score_variable] =
(scoreCounts[selectedStatement.score_variable] || 0) + 1;
}
} else if (question.questionType === QuestionType.AgreementQuestion) {
const scoreVariable = (question as AgreementQuestion).score_variable;
scoreCounts[scoreVariable] = (scoreCounts[scoreVariable] || 0) + answer;
}
}
});
const maxScoreVariable = Object.keys(scoreCounts).reduce((a, b) =>
scoreCounts[a] > scoreCounts[b] ? a : b
);
navigate(`/surveys/results/${maxScoreVariable}`);
};
return (
<div className="survey-page">
<LoadingData data={survey}>
<h1>
{survey?.title}
</h1>
<div>
{survey?.description}
</div>
<h1>{survey?.title}</h1>
<div>{survey?.description}</div>
<SurveyQuestions survey={survey}/>
<LoadingList data={questions} listElement={(question: Question) => (
<QuestionBlock
key={`${question.questionType}_${question.id}`} // Используем тип вопроса и id для уникальности
question={question}
onAnswer={handleAnswer}
answered={answers[`${question.questionType}_${question.id}`]} // Учитываем тип вопроса в ключе
/>
)} />
{isCompleted && (
<button onClick={calculateResults}>Завершить тест</button>
)}
</LoadingData>
</div>
);
}
function QuestionBlock({
question,
onAnswer,
answered,
}: {
question: Question;
onAnswer: (questionId: string, answerValue: number) => void;
answered?: number;
}) {
switch (question.questionType) {
case QuestionType.AgreementQuestion:
const agreementQuestion = question as AgreementQuestion;
return (
<div className={`question agreement ${answered !== undefined ? "answered" : "unanswered"}`}>
{agreementQuestion.question}
<button
className={`answer ${answered === 1 ? "selected" : "idle"}`}
disabled={answered===1}
onClick={() => onAnswer(`${question.questionType}_${question.id}`, 1)}>Согласен</button>
<button
className={`answer ${answered === 0 ? "selected" : "idle"}`}
disabled={answered === 0}
onClick={() => onAnswer(`${question.questionType}_${question.id}`, 0)}>Не согласен</button>
function SurveyQuestions({survey}: { survey: Survey | null | undefined }) {
</div>
);
const _survey = survey as Survey;
case QuestionType.QuestionPair:
const pairQuestion = question as QuestionPair;
return (
<div className={`question pair ${answered !== undefined ? "answered" : "unanswered"}`}>
{pairQuestion.statements.map((statement) => (
<button
className={`answer ${answered === statement.id ? "selected" : "idle"}`}
disabled={answered === statement.id}
key={statement.id}
onClick={() => onAnswer(`${question.questionType}_${question.id}`, statement.id)}
>
{statement.text}
</button>
))}
</div>
);
const questionsPairs = useDataPage("surveys", "question-pairs", {survey:_survey.id});
const questionsAgreement = useDataPage("surveys", "question-pairs", {survey:_survey.id});
//const questions =
return (
<>
</>
);
default:
return null;
}
}

View File

@@ -0,0 +1,45 @@
import { Link, useParams } from 'react-router-dom';
import { useData, useDataPage } from '../API/common';
import { Profession, ScoreVariable, Survey } from '../types/survey';
import ReactMarkdown from 'react-markdown';
import { LoadingData } from '../components/LoadingData';
import { LoadingList } from '../components/LoadingList';
export function SurveyResultPage() {
const { scoreVariableId } = useParams<{ scoreVariableId: string }>();
const scoreVariable = useData<ScoreVariable>('surveys', 'score-variables', scoreVariableId || 0);
const survey = useData<Survey>('surveys', 'surveys', scoreVariable?.survey || 0, !!scoreVariable);
const professions = useDataPage<Profession>(
'surveys',
'professions',
{ score_variable: scoreVariableId || 0 },
!!scoreVariable
);
return (
<div>
<LoadingData data={survey && scoreVariable?survey:scoreVariable}>
<h1>{survey?.title}</h1>
<div className={"result"}>
<h2>Ваш результат</h2>
<p>{scoreVariable?.title}</p>
</div>
<div className={"description"}>
<ReactMarkdown>{scoreVariable?.description}</ReactMarkdown>
</div>
<div className={"profession-list"}>
<h3>Подходящие профессии</h3>
<LoadingList
data={professions.items}
listElement={(profession)=>(
<Link key={profession.id} to={`/surveys/professions/${profession.id}/`} className={"profession"}>
{(profession as Profession).name}
</Link>
)}
/>
</div>
</LoadingData>
</div>
);
}

View File

@@ -11,6 +11,47 @@ interface Statement extends UniqueItem{
score_variable: number;
}
export interface QuestionPair extends UniqueItem{
export enum QuestionType{
QuestionPair,
AgreementQuestion
}
export interface Question extends UniqueItem{
questionType: QuestionType
}
export interface QuestionPair extends Question{
statements: Statement[]
}
export interface AgreementQuestion extends Question{
question: string;
score_variable: number;
}
export interface ScoreVariable {
id: string;
name: string;
title: string;
description: string;
survey: number;
}
export interface Profession extends UniqueItem {
name: string;
description: string;
}
export interface Specialty extends UniqueItem {
name: string;
institution: number;
profession: number;
}
export interface Institution extends UniqueItem {
name: string;
}