Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Модуль 8 «Оптимизация производительности» #8

Merged
merged 12 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/components/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Route, Routes} from 'react-router-dom';
import {HelmetProvider} from 'react-helmet-async';
import {useAppSelector} from '../../hooks';
import {AppRoute, AuthorizationStatus, MAX_MISTAKE_COUNT} from '../../const';
import {AppRoute, MAX_MISTAKE_COUNT} from '../../const';
import WelcomeScreen from '../../pages/welcome-screen/welcome-screen';
import AuthScreen from '../../pages/auth-screen/auth-screen';
import GameOverScreen from '../../pages/game-over-screen/game-over-screen';
Expand All @@ -10,19 +10,29 @@ import NotFoundScreen from '../../pages/not-found-screen/not-found-screen';
import PrivateRoute from '../private-route/private-route';
import GameScreen from '../../pages/game-screen/game-screen';
import LoadingScreen from '../../pages/loading-screen/loading-screen';
import ErrorScreen from '../../pages/error-screen/error-screen';
import HistoryRouter from '../history-route/history-route';
import browserHistory from '../../browser-history';
import {getAuthorizationStatus, getAuthCheckedStatus} from '../../store/user-process/selectors';
import {getQuestionsDataLoadingStatus, getErrorStatus} from '../../store/game-data/selectors';

function App(): JSX.Element {
const authorizationStatus = useAppSelector((state) => state.authorizationStatus);
const isQuestionsDataLoading = useAppSelector((state) => state.isQuestionsDataLoading);
const authorizationStatus = useAppSelector(getAuthorizationStatus);
const isAuthChecked = useAppSelector(getAuthCheckedStatus);
const isQuestionsDataLoading = useAppSelector(getQuestionsDataLoadingStatus);
const hasError = useAppSelector(getErrorStatus);

if (authorizationStatus === AuthorizationStatus.Unknown || isQuestionsDataLoading) {
if (!isAuthChecked || isQuestionsDataLoading) {
return (
<LoadingScreen />
);
}

if (hasError) {
return (
<ErrorScreen />);
}

return (
<HelmetProvider>
<HistoryRouter history={browserHistory}>
Expand Down
42 changes: 42 additions & 0 deletions src/components/genre-question-item/genre-question-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {ChangeEvent} from 'react';
import {GenreAnswer} from '../../types/question';

type GenreQuestionItemProps = {
answer: GenreAnswer;
id: number;
onChange: (id: number, value: boolean) => void;
renderPlayer: (path: string, playerIndex: number) => JSX.Element;
userAnswer: boolean;
}

function GenreQuestionItem(props: GenreQuestionItemProps): JSX.Element {
const {answer, id, onChange, renderPlayer, userAnswer} = props;

return (
<div className="track">
{renderPlayer(answer.src, id)}
<div className="game__answer">
<input
className="game__input visually-hidden"
type="checkbox"
name="answer"
value={`answer-${id}`}
id={`answer-${id}`}
checked={userAnswer}
onChange={({target}: ChangeEvent<HTMLInputElement>) => {
const value = target.checked;
onChange(id, value);
}}
/>
<label
className="game__check"
htmlFor={`answer-${id}`}
>
Отметить
</label>
</div>
</div>
);
}

export default GenreQuestionItem;
44 changes: 44 additions & 0 deletions src/components/genre-question-list/genre-question-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {FormEvent} from 'react';
import GenreQuestionItem from '../genre-question-item/genre-question-item';
import {useUserAnswers} from '../../hooks/use-user-answers';
import {QuestionGenre, UserGenreQuestionAnswer} from '../../types/question';

type GenreQuestionListProps = {
question: QuestionGenre;
onAnswer: (question: QuestionGenre, answers: UserGenreQuestionAnswer) => void;
renderPlayer: (src: string, playerIndex: number) => JSX.Element;
};

function GenreQuestionList(props: GenreQuestionListProps) {
const {question, onAnswer, renderPlayer} = props;
const {answers} = question;
const [userAnswers, handleAnswerChange] = useUserAnswers(question);

return (
<form
className="game__tracks"
onSubmit={(evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
onAnswer(question, userAnswers);
}}
>
{answers.map((answer, id) => {
const keyValue = `${id}-${answer.src}`;
return (
<GenreQuestionItem
answer={answer}
id={id}
key={keyValue}
onChange={handleAnswerChange}
renderPlayer={renderPlayer}
userAnswer={userAnswers[id]}
/>
);
})}

<button className="game__submit button" type="submit">Ответить</button>
</form>
);
}

export default GenreQuestionList;
6 changes: 6 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ export enum APIRoute {
Login = '/login',
Logout = '/logout',
}

export enum NameSpace {
Data = 'DATA',
Game = 'GAME',
User = 'USER',
}
18 changes: 18 additions & 0 deletions src/hooks/use-user-answers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {useState} from 'react';
import {QuestionGenre} from '../types/question';

type ResultUserAnswers = [boolean[], (id: number, value: boolean) => void];

export const useUserAnswers = (question: QuestionGenre): ResultUserAnswers => {
const answersCount = question.answers.length;

const [answers, setAnswers] = useState<boolean[]>(Array.from({length: answersCount}, () => false));

const handleAnswerChange = (id: number, value: boolean) => {
const userAnswers = answers.slice(0);
userAnswers[id] = value;
setAnswers(userAnswers);
};

return [answers, handleAnswerChange];
};
23 changes: 23 additions & 0 deletions src/pages/error-screen/error-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {useAppDispatch} from '../../hooks';
import {fetchQuestionAction} from '../../store/api-actions';

function ErrorScreen(): JSX.Element {
const dispatch = useAppDispatch();

return (
<>
<p className="error__text">Не удалось загрузить вопросы</p>
<button
onClick={() => {
dispatch(fetchQuestionAction());
}}
className="replay replay--error"
type="button"
>
Попробовать ещё раз
</button>
</>
);
}

export default ErrorScreen;
2 changes: 1 addition & 1 deletion src/pages/game-over-screen/game-over-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Helmet} from 'react-helmet-async';
import {useNavigate} from 'react-router-dom';
import {useAppDispatch} from '../../hooks';
import {resetGame} from '../../store/action';
import {resetGame} from '../../store/game-process/game-process';
import {AppRoute} from '../../const';

function GameOverScreen(): JSX.Element {
Expand Down
10 changes: 6 additions & 4 deletions src/pages/game-screen/game-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import {Navigate} from 'react-router-dom';
import {useAppDispatch, useAppSelector} from '../../hooks';
import {incrementStep, checkUserAnswer} from '../../store/action';
import {incrementStep, checkUserAnswer} from '../../store/game-process/game-process';
import {AppRoute, GameType, MAX_MISTAKE_COUNT} from '../../const';
import ArtistQuestionScreen from '../artist-question-screen/artist-question-screen';
import GenreQuestionScreen from '../genre-question-screen/genre-question-screen';
import Mistakes from '../../components/mistakes/mistakes';
import {Question, UserAnswer} from '../../types/question';
import withAudioPlayer from '../../hocs/with-audio-player/with-audio-player';
import {getMistakeCount, getStep} from '../../store/game-process/selectors';
import {getQuestions} from '../../store/game-data/selectors';

const ArtistQuestionScreenWrapped = withAudioPlayer(ArtistQuestionScreen);
const GenreQuestionScreenWrapped = withAudioPlayer(GenreQuestionScreen);

function GameScreen(): JSX.Element {
const step = useAppSelector((state) => state.step);
const mistakes = useAppSelector((state) => state.mistakes);
const questions = useAppSelector((state) => state.questions);
const step = useAppSelector(getStep);
const mistakes = useAppSelector(getMistakeCount);
const questions = useAppSelector(getQuestions);

const question = questions[step];

Expand Down
41 changes: 8 additions & 33 deletions src/pages/genre-question-screen/genre-question-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useState, FormEvent, ChangeEvent, PropsWithChildren} from 'react';
import {PropsWithChildren} from 'react';
import {Helmet} from 'react-helmet-async';
import Logo from '../../components/logo/logo';
import GenreQuestionList from '../../components/genre-question-list/genre-question-list';
import {QuestionGenre, UserGenreQuestionAnswer} from '../../types/question';

type GenreQuestionScreenProps = PropsWithChildren<{
Expand All @@ -11,9 +12,7 @@ type GenreQuestionScreenProps = PropsWithChildren<{

function GenreQuestionScreen(props: GenreQuestionScreenProps): JSX.Element {
const {question, onAnswer, renderPlayer, children} = props;
const {answers, genre} = question;

const [userAnswers, setUserAnswers] = useState([false, false, false, false]);
const {genre} = question;

return (
<section className="game game--genre">
Expand All @@ -34,35 +33,11 @@ function GenreQuestionScreen(props: GenreQuestionScreenProps): JSX.Element {

<section className="game__screen">
<h2 className="game__title">Выберите {genre} треки</h2>
<form
className="game__tracks"
onSubmit={(evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
onAnswer(question, userAnswers);
}}
>
{answers.map((answer, id) => {
const keyValue = `${id}-${answer.src}`;
return (
<div key={keyValue} className="track">
{renderPlayer(answer.src, id)}
<div className="game__answer">
<input className="game__input visually-hidden" type="checkbox" name="answer" value={`answer-${id}`}
id={`answer-${id}`}
checked={userAnswers[id]}
onChange={({target}: ChangeEvent<HTMLInputElement>) => {
const value = target.checked;
setUserAnswers([...userAnswers.slice(0, id), value, ...userAnswers.slice(id + 1)]);
}}
/>
<label className="game__check" htmlFor={`answer-${id}`}>Отметить</label>
</div>
</div>
);
})}

<button className="game__submit button" type="submit">Ответить</button>
</form>
<GenreQuestionList
question={question}
onAnswer={onAnswer}
renderPlayer={renderPlayer}
/>
</section>
</section>
);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/welcome-screen/welcome-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useNavigate} from 'react-router-dom';
import {Helmet} from 'react-helmet-async';
import {useAppDispatch} from '../../hooks';
import {resetGame} from '../../store/action';
import {resetGame} from '../../store/game-process/game-process';
import {AppRoute} from '../../const';

type WelcomeScreenProps = {
Expand Down
7 changes: 4 additions & 3 deletions src/pages/win-screen/win-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {Link, useNavigate} from 'react-router-dom';
import {Helmet} from 'react-helmet-async';
import {useAppSelector, useAppDispatch} from '../../hooks';
import {resetGame} from '../../store/action';
import {resetGame} from '../../store/game-process/game-process';
import {logoutAction} from '../../store/api-actions';
import {AppRoute} from '../../const';
import {getMistakeCount, getStep} from '../../store/game-process/selectors';

function WinScreen(): JSX.Element {
const step = useAppSelector((state) => state.step);
const mistakes = useAppSelector((state) => state.mistakes);
const step = useAppSelector(getStep);
const mistakes = useAppSelector(getMistakeCount);

const dispatch = useAppDispatch();
const navigate = useNavigate();
Expand Down
15 changes: 1 addition & 14 deletions src/store/action.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
import {createAction} from '@reduxjs/toolkit';
import {Question, Questions, UserAnswer} from '../types/question';
import {AppRoute, AuthorizationStatus} from '../const';

export const incrementStep = createAction('game/incrementStep');

export const checkUserAnswer = createAction<{question: Question; userAnswer: UserAnswer}>('game/checkUserAnswer');

export const resetGame = createAction('game/reset');

export const loadQuestions = createAction<Questions>('data/loadQuestions');

export const setQuestionsDataLoadingStatus = createAction<boolean>('data/setQuestionsDataLoadingStatus');

export const requireAuthorization = createAction<AuthorizationStatus>('user/requireAuthorization');
import {AppRoute} from '../const';

export const redirectToRoute = createAction<AppRoute>('game/redirectToRoute');
25 changes: 8 additions & 17 deletions src/store/api-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@ import {AxiosInstance} from 'axios';
import {createAsyncThunk} from '@reduxjs/toolkit';
import {AppDispatch, State} from '../types/state.js';
import {Questions} from '../types/question';
import {loadQuestions, requireAuthorization, setQuestionsDataLoadingStatus, redirectToRoute} from './action';
import {redirectToRoute} from './action';
import {saveToken, dropToken} from '../services/token';
import {APIRoute, AuthorizationStatus, AppRoute} from '../const';
import {APIRoute, AppRoute} from '../const';
import {AuthData} from '../types/auth-data';
import {UserData} from '../types/user-data';

export const fetchQuestionAction = createAsyncThunk<void, undefined, {
export const fetchQuestionAction = createAsyncThunk<Questions, undefined, {
dispatch: AppDispatch;
state: State;
extra: AxiosInstance;
}>(
'data/fetchQuestions',
async (_arg, {dispatch, extra: api}) => {
dispatch(setQuestionsDataLoadingStatus(true));
async (_arg, {extra: api}) => {
const {data} = await api.get<Questions>(APIRoute.Questions);
dispatch(setQuestionsDataLoadingStatus(false));
dispatch(loadQuestions(data));
return data;
},
);

Expand All @@ -28,13 +26,8 @@ export const checkAuthAction = createAsyncThunk<void, undefined, {
extra: AxiosInstance;
}>(
'user/checkAuth',
async (_arg, {dispatch, extra: api}) => {
try {
await api.get(APIRoute.Login);
dispatch(requireAuthorization(AuthorizationStatus.Auth));
} catch {
dispatch(requireAuthorization(AuthorizationStatus.NoAuth));
}
async (_arg, {extra: api}) => {
await api.get(APIRoute.Login);
},
);

Expand All @@ -47,7 +40,6 @@ export const loginAction = createAsyncThunk<void, AuthData, {
async ({login: email, password}, {dispatch, extra: api}) => {
const {data: {token}} = await api.post<UserData>(APIRoute.Login, {email, password});
saveToken(token);
dispatch(requireAuthorization(AuthorizationStatus.Auth));
dispatch(redirectToRoute(AppRoute.Result));
},
);
Expand All @@ -58,10 +50,9 @@ export const logoutAction = createAsyncThunk<void, undefined, {
extra: AxiosInstance;
}>(
'user/logout',
async (_arg, {dispatch, extra: api}) => {
async (_arg, {extra: api}) => {
await api.delete(APIRoute.Logout);
dropToken();
dispatch(requireAuthorization(AuthorizationStatus.NoAuth));
},
);

Loading