Compare commits
2 Commits
9c6272db25
...
b5b2439bd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5b2439bd8 | ||
|
|
35ed595edc |
@@ -1,7 +1,8 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useMemo, useState} from "react";
|
||||||
import {DEBUG_MODE} from "../utils/common";
|
import {DEBUG_MODE} from "../utils/common";
|
||||||
import {ApiResponse, PageResponse} from "../types/api.ts";
|
import {ApiResponse, PageResponse} from "../types/api.ts";
|
||||||
import {PageControl} from "../types/common.ts";
|
import {PageControl} from "../types/common.ts";
|
||||||
|
import {Institution} from "../types/survey.ts";
|
||||||
|
|
||||||
export function hostUrl(url: string){
|
export function hostUrl(url: string){
|
||||||
return DEBUG_MODE? "http://localhost:8000"+url: url;
|
return DEBUG_MODE? "http://localhost:8000"+url: url;
|
||||||
@@ -144,3 +145,43 @@ export function useData<T>(application:string, endpoint: string, id: string|numb
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCachedData<T>(application: string, endpoint: string) {
|
||||||
|
const [cache, setCache] = useState<Map<string | number, T | undefined>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Очистка кеша или другие побочные эффекты при необходимости
|
||||||
|
return () => {
|
||||||
|
setCache(new Map());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (id: string | number, needLoad: boolean = true): T | undefined | null => {
|
||||||
|
const cachedData = cache.has(id)?cache.get(id):null;
|
||||||
|
|
||||||
|
if (cachedData !== null) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needLoad) {
|
||||||
|
getFetch(`/${application}/${endpoint}/${id}`).then((r) => {
|
||||||
|
if (r.success) {
|
||||||
|
const data = r.body as T;
|
||||||
|
setCache((prevCache) => {
|
||||||
|
const newCache = new Map(prevCache);
|
||||||
|
newCache.set(id, data);
|
||||||
|
return newCache;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCache((prevCache) => {
|
||||||
|
const newCache = new Map(prevCache);
|
||||||
|
newCache.set(id, undefined);
|
||||||
|
return newCache;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
78
src/App.tsx
78
src/App.tsx
@@ -1,28 +1,84 @@
|
|||||||
// App.tsx
|
// App.tsx
|
||||||
import {BrowserRouter as Router, Routes, Route, Navigate, Link} from 'react-router-dom';
|
import {BrowserRouter as Router, Routes, Route, Link, Navigate, Outlet, useNavigate} from 'react-router-dom';
|
||||||
import {MainPage} from "./pages/MainPage.tsx";
|
import {MainPage} from "./pages/MainPage.tsx";
|
||||||
import {SurveyPage} from "./pages/SurveyPage.tsx";
|
import {SurveyPage} from "./pages/SurveyPage.tsx";
|
||||||
import {DashboardPage} from "./pages/DashboardPage.tsx";
|
import {DashboardPage} from "./pages/DashboardPage.tsx";
|
||||||
import { SurveyResultPage } from './pages/SurveyResultPage.tsx';
|
import { SurveyResultPage } from './pages/SurveyResultPage.tsx';
|
||||||
import { ProfessionPage } from './pages/ProfessionPage.tsx';
|
import { ProfessionPage } from './pages/ProfessionPage.tsx';
|
||||||
|
import React, {useEffect} from "react";
|
||||||
|
import {LoginPage} from "./pages/LoginPage.tsx";
|
||||||
|
import {UserProvider} from "./utils/users/UserProvider.tsx";
|
||||||
|
import {useUser} from "./utils/users/UseUser.ts";
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<UserProvider>
|
||||||
<Router>
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
|
||||||
|
<Route path="/login/" element={<LoginPage />} />
|
||||||
|
|
||||||
|
|
||||||
|
<Route path="/dashboard/*" element={<DashboardLayout />}>
|
||||||
|
<Route index element={<DashboardPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
|
||||||
|
<Route path="/" element={<MainLayout />}>
|
||||||
|
<Route index element={<MainPage />} />
|
||||||
|
<Route path="/surveys/results/:scoreVariableId/" element={<SurveyResultPage />} />
|
||||||
|
<Route path="/surveys/professions/:professionId/" element={<ProfessionPage />} />
|
||||||
|
<Route path="/surveys/:surveyId/" element={<SurveyPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="*" element={<Navigate to="/" />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</UserProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function DashboardLayout() {
|
||||||
|
const { user } = useUser();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
navigate("/login/")
|
||||||
|
}
|
||||||
|
}, [navigate, user]);
|
||||||
|
|
||||||
|
if (!user){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="header">
|
||||||
|
<h1 className="logo">Pro-Fi</h1>
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function MainLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<Link to="/dashboard" className="menu-button">☰</Link>
|
<Link to="/dashboard" className="menu-button">☰</Link>
|
||||||
<h1 className="logo">Pro-Fi Test</h1>
|
<h1 className="logo">Pro-Fi Test</h1>
|
||||||
</div>
|
</div>
|
||||||
<Routes>
|
<div className="content">
|
||||||
<Route path="/" element={<MainPage/>}/>
|
<Outlet />
|
||||||
<Route path="/surveys/results/:scoreVariableId/" element={<SurveyResultPage/>}/>
|
</div>
|
||||||
<Route path="/surveys/professions/:professionId/" element={<ProfessionPage/>}/>
|
</>
|
||||||
<Route path="/surveys/:surveyId/" element={<SurveyPage/>}/>
|
|
||||||
<Route path="/dashboard/" element={<DashboardPage/>}/>
|
|
||||||
{/* <Route path="*" element={<Navigate to="/"/>}/> */}
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {ReactElement} from "react";
|
|
||||||
import {UniqueItem} from "../types/common.ts";
|
import {UniqueItem} from "../types/common.ts";
|
||||||
|
import {JSX} from "react";
|
||||||
|
|
||||||
export function LoadingList({data, listElement}: {data: UniqueItem[]|null, listElement(e:UniqueItem, i:number):ReactElement}) {
|
export function LoadingList({data, listElement}: {data: UniqueItem[]|null, listElement(e:UniqueItem, i:number):unknown}) {
|
||||||
if (data === null){
|
if (data === null){
|
||||||
return <div className={"loading"}>Идёт загрузка...</div>
|
return <div className={"loading"}>Идёт загрузка...</div>
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/pages/LoginPage.tsx
Normal file
3
src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function LoginPage() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import {Link, useParams} from 'react-router-dom';
|
||||||
import { useData, useDataPage } from '../API/common';
|
import {useCachedData, useData, useDataPage} from '../API/common';
|
||||||
import { Institution, Profession, Specialty } from '../types/survey';
|
import { Institution, Profession, Specialty } from '../types/survey';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { LoadingData } from '../components/LoadingData';
|
import { LoadingData } from '../components/LoadingData';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { LoadingList } from '../components/LoadingList';
|
import { LoadingList } from '../components/LoadingList';
|
||||||
|
|
||||||
|
|
||||||
@@ -13,29 +12,7 @@ export function ProfessionPage() {
|
|||||||
const profession = useData<Profession>('surveys', 'professions', professionId || 0);
|
const profession = useData<Profession>('surveys', 'professions', professionId || 0);
|
||||||
const specialties = useDataPage<Specialty>('surveys', 'specialties', { profession: professionId || 0 }, !!profession);
|
const specialties = useDataPage<Specialty>('surveys', 'specialties', { profession: professionId || 0 }, !!profession);
|
||||||
|
|
||||||
const [institutionCache, setInstitutionCache] = useState<Map<number, Institution>>(new Map()); // Кеширование учебных заведений
|
const institutions = useCachedData<Institution>("surveys", "institutions");
|
||||||
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -45,14 +22,14 @@ export function ProfessionPage() {
|
|||||||
<ReactMarkdown>{profession?.description || ''}</ReactMarkdown>
|
<ReactMarkdown>{profession?.description || ''}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
<div className="specialties-block">
|
<div className="specialties-block">
|
||||||
<h2>Где можно обучиться</h2>
|
{specialties.items != null ? <h2>Где можно обучиться</h2>:null}
|
||||||
<LoadingList
|
<LoadingList
|
||||||
data={specialties.items}
|
data={specialties.items}
|
||||||
listElement={(specialty) => (
|
listElement={specialty => (
|
||||||
<SpecialtyBlock
|
<SpecialtyBlock
|
||||||
key={specialty.id}
|
key={specialty.id}
|
||||||
specialty={specialty as Specialty}
|
specialty={specialty as Specialty}
|
||||||
institution={institutionCache.get((specialty as Specialty).institution)}
|
institution={institutions((specialty as Specialty).institution)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -67,12 +44,14 @@ function SpecialtyBlock({
|
|||||||
institution,
|
institution,
|
||||||
}: {
|
}: {
|
||||||
specialty: Specialty;
|
specialty: Specialty;
|
||||||
institution: Institution | undefined;
|
institution: Institution | undefined | null;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="specialty-block">
|
<div className="specialty-block">
|
||||||
<h3>{specialty.name}</h3>
|
<h3>Учебное заведение: {institution?.name || 'Загрузка...'}</h3>
|
||||||
<p>Учебное заведение: {institution?.name || 'Загрузка...'}</p>
|
<p className={"specialty-name"}>{specialty.name}</p>
|
||||||
|
{specialty.link?<Link to={specialty.link} className={"site"}>Открыть сайт</Link>:null}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -106,9 +106,11 @@ export function SurveyPage() {
|
|||||||
/>
|
/>
|
||||||
)} />
|
)} />
|
||||||
|
|
||||||
|
<>
|
||||||
{isCompleted && (
|
{isCompleted && (
|
||||||
<button onClick={calculateResults}>Завершить тест</button>
|
<button onClick={calculateResults}>Завершить тест</button>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
</LoadingData>
|
</LoadingData>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -125,7 +127,7 @@ function QuestionBlock({
|
|||||||
}) {
|
}) {
|
||||||
switch (question.questionType) {
|
switch (question.questionType) {
|
||||||
case QuestionType.AgreementQuestion:
|
case QuestionType.AgreementQuestion:
|
||||||
const agreementQuestion = question as AgreementQuestion;
|
{ const agreementQuestion = question as AgreementQuestion;
|
||||||
return (
|
return (
|
||||||
<div className={`question agreement ${answered !== undefined ? "answered" : "unanswered"}`}>
|
<div className={`question agreement ${answered !== undefined ? "answered" : "unanswered"}`}>
|
||||||
{agreementQuestion.question}
|
{agreementQuestion.question}
|
||||||
@@ -139,10 +141,10 @@ function QuestionBlock({
|
|||||||
onClick={() => onAnswer(`${question.questionType}_${question.id}`, 0)}>Не согласен</button>
|
onClick={() => onAnswer(`${question.questionType}_${question.id}`, 0)}>Не согласен</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
); }
|
||||||
|
|
||||||
case QuestionType.QuestionPair:
|
case QuestionType.QuestionPair:
|
||||||
const pairQuestion = question as QuestionPair;
|
{ const pairQuestion = question as QuestionPair;
|
||||||
return (
|
return (
|
||||||
<div className={`question pair ${answered !== undefined ? "answered" : "unanswered"}`}>
|
<div className={`question pair ${answered !== undefined ? "answered" : "unanswered"}`}>
|
||||||
{pairQuestion.statements.map((statement) => (
|
{pairQuestion.statements.map((statement) => (
|
||||||
@@ -156,7 +158,7 @@ function QuestionBlock({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
); }
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ export interface PageControl<T>{
|
|||||||
nextPage(): void;
|
nextPage(): void;
|
||||||
previousPage(): void;
|
previousPage(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CachedData<T>{
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export interface Profession extends UniqueItem {
|
|||||||
export interface Specialty extends UniqueItem {
|
export interface Specialty extends UniqueItem {
|
||||||
name: string;
|
name: string;
|
||||||
institution: number;
|
institution: number;
|
||||||
profession: number;
|
link?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Institution extends UniqueItem {
|
export interface Institution extends UniqueItem {
|
||||||
|
|||||||
22
src/types/users.ts
Normal file
22
src/types/users.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {UniqueItem} from "./common.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export interface ContactData{
|
||||||
|
first_name: string;
|
||||||
|
last_name?: string;
|
||||||
|
middle_name?: string;
|
||||||
|
phone?: string;
|
||||||
|
email?:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Role extends UniqueItem{
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Account extends UniqueItem, ContactData{
|
||||||
|
login: string;
|
||||||
|
role?: Role;
|
||||||
|
is_staff: boolean;
|
||||||
|
is_active: boolean;
|
||||||
|
|
||||||
|
}
|
||||||
13
src/utils/users/UseUser.ts
Normal file
13
src/utils/users/UseUser.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import {UserContext} from "./UserContext.ts";
|
||||||
|
import {useContext} from "react";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function useUser() {
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useUser must be used within a UserProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
10
src/utils/users/UserContext.ts
Normal file
10
src/utils/users/UserContext.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {Account} from "../../types/users.ts";
|
||||||
|
import {createContext} from "react";
|
||||||
|
|
||||||
|
export type UserContextType = {
|
||||||
|
user: Account | undefined;
|
||||||
|
login: (userData: Account) => void;
|
||||||
|
logout: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
35
src/utils/users/UserProvider.tsx
Normal file
35
src/utils/users/UserProvider.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, {ReactNode, useEffect, useState} from 'react';
|
||||||
|
import {Account} from '../../types/users.ts';
|
||||||
|
import {UserContext} from "./UserContext.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export function UserProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [user, setUser] = useState<Account | undefined>(() => {
|
||||||
|
const storedUser = localStorage.getItem('user');
|
||||||
|
return storedUser ? JSON.parse(storedUser) : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
document.cookie = `user=${encodeURIComponent(JSON.stringify(user))}; path=/;`;
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
document.cookie = 'user=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;';
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const login = (userData: Account) => {
|
||||||
|
setUser(userData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
setUser(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Provider value={{ user, login, logout }}>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user