diff --git a/package-lock.json b/package-lock.json index 2a73dd16f..77e5e6252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "cm6-theme-basic-dark": "^0.2.0", "cm6-theme-basic-light": "^0.2.0", "codemirror": "^6.0.1", + "date-fns": "^2.29.3", "highlight.js": "^11.x", "i18next": "^21.5.4", "jquery": "^3.6.0", @@ -26,6 +27,7 @@ "lodash": "^4.17.21", "react": "^17.0.2", "react-bootstrap": "^2.7.0", + "react-countdown": "^2.3.5", "react-dom": "^17.0.2", "react-i18next": "^11.14.3", "react-redux": "^7.2.6", @@ -4426,6 +4428,18 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "dev": true, @@ -9273,6 +9287,18 @@ } } }, + "node_modules/react-countdown": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.5.tgz", + "integrity": "sha512-K26ENYEesMfPxhRRtm1r+Pf70SErrvW3g4CArLi/x6MPFjgfDFYePT4UghEj8p2nI0cqVV7/JjDgjyr//U60Og==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">= 15", + "react-dom": ">= 15" + } + }, "node_modules/react-dom": { "version": "17.0.2", "license": "MIT", @@ -14650,6 +14676,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + }, "debug": { "version": "4.3.4", "dev": true, @@ -17773,6 +17804,14 @@ "warning": "^4.0.3" } }, + "react-countdown": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.5.tgz", + "integrity": "sha512-K26ENYEesMfPxhRRtm1r+Pf70SErrvW3g4CArLi/x6MPFjgfDFYePT4UghEj8p2nI0cqVV7/JjDgjyr//U60Og==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-dom": { "version": "17.0.2", "requires": { diff --git a/package.json b/package.json index 58ec246c5..25a7323b2 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "cm6-theme-basic-dark": "^0.2.0", "cm6-theme-basic-light": "^0.2.0", "codemirror": "^6.0.1", + "date-fns": "^2.29.3", "highlight.js": "^11.x", "i18next": "^21.5.4", "jquery": "^3.6.0", @@ -36,6 +37,7 @@ "lodash": "^4.17.21", "react": "^17.0.2", "react-bootstrap": "^2.7.0", + "react-countdown": "^2.3.5", "react-dom": "^17.0.2", "react-i18next": "^11.14.3", "react-redux": "^7.2.6", diff --git a/resources/assets/images/waiting_clock.png b/resources/assets/images/waiting_clock.png new file mode 100644 index 000000000..8ba71a627 Binary files /dev/null and b/resources/assets/images/waiting_clock.png differ diff --git a/resources/js/components/TeacherSolution.jsx b/resources/js/components/TeacherSolution.jsx index 6cd7b780b..4cd0f9522 100644 --- a/resources/js/components/TeacherSolution.jsx +++ b/resources/js/components/TeacherSolution.jsx @@ -1,13 +1,63 @@ import React from 'react'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; +import Countdown from 'react-countdown'; +import { format } from 'date-fns'; import Highlight from 'react-syntax-highlighter'; import { vs, monokaiSublime } from 'react-syntax-highlighter/dist/esm/styles/hljs'; +import { useTranslation } from 'react-i18next'; + +import { changeShowStatus } from '../slices/solutionSlice'; +import waitingClock from '../../assets/images/waiting_clock.png'; import theme from '../common/currentTheme'; const TeacherSolution = () => { + const dispatch = useDispatch(); + const { t } = useTranslation(); const { hasTeacherSolution, teacherSolutionCode } = useSelector((state) => state.exerciseInfo); + const { + displaySolutionState, + startTime, + waitingTime, + } = useSelector((state) => state.showSolution); + + const handleShowSolution = () => { + dispatch(changeShowStatus('isShown')); + }; + + const renderShowButton = () => ( + <> +

{t('solutionNotice')}

+
+ +
+ + ); + + const renderCountdown = (countdownData) => { + const { completed } = countdownData; + + if (completed || displaySolutionState === 'canBeShown') { + return renderShowButton(); + } - return hasTeacherSolution ? ( + const remainingTime = format(new Date(countdownData.total), 'mm:ss'); + + return ( +
+

{t('solutionInstructions')}

+
{ remainingTime }
+ waiting_clock +
+ ); + }; + + const renderShowSolution = () => (displaySolutionState === 'isShown' ? (
{ {teacherSolutionCode}
- ) : null; + ) : ( +
+ +
+ )); + + return hasTeacherSolution ? renderShowSolution() : null; }; export default TeacherSolution; diff --git a/resources/js/components/store.js b/resources/js/components/store.js index 1c66cace8..542208553 100644 --- a/resources/js/components/store.js +++ b/resources/js/components/store.js @@ -2,6 +2,7 @@ import { configureStore } from '@reduxjs/toolkit'; import editorReducer from '../slices/editorSlice.js'; import tabsBoxReducer from '../slices/tabsBoxSlice.js'; import checkResultReducer from '../slices/checkResultSlice.js'; +import solutionReducer from '../slices/solutionSlice.js'; import exerciseInfoReducer from '../slices/exerciseInfoSlice.js'; import notificationReducer from '../slices/notificationSlice.js'; @@ -11,6 +12,7 @@ export default () => { editor: editorReducer, tabsBox: tabsBoxReducer, checkResult: checkResultReducer, + showSolution: solutionReducer, exerciseInfo: exerciseInfoReducer, notification: notificationReducer, }, diff --git a/resources/js/locales/en.js b/resources/js/locales/en.js index 0f457cc3d..31aaf2bf5 100644 --- a/resources/js/locales/en.js +++ b/resources/js/locales/en.js @@ -7,6 +7,9 @@ export default { tests: 'Tests', loading: 'Loading...', teacherSolution: 'Solution', + solutionInstructions: "Teacher's solution will be available in:", + solutionNotice: "It's best to solve the problem yourself, but if you're stuck for a long time, feel free to check out the solution. But make sure to study it thoroughly to truly understand it.", + showSolution: 'Show solution', editorContent: { withTemplate: 'Write your solution here', withoutTemplate: 'This exercise has no tests.\nAny solution is a right answer.', diff --git a/resources/js/locales/ru.js b/resources/js/locales/ru.js index 704cebbe6..317956d06 100644 --- a/resources/js/locales/ru.js +++ b/resources/js/locales/ru.js @@ -7,6 +7,9 @@ export default { tests: 'Тесты', loading: 'Загрузка...', teacherSolution: 'Решение', + solutionInstructions: 'Решение учителя откроется через:', + solutionNotice: 'Желательно решить задачу самостоятельно, но если вы застряли и долгое время ничего не получается, посмотрите решение учителя. Но обязательно разберитесь в нём и повторите по памяти', + showSolution: 'Показать решение', editorContent: { withTemplate: 'Введите свое решение', withoutTemplate: 'Для этого упражнения нет проверок.\nЛюбое решение будет считаться успешным ответом.', diff --git a/resources/js/slices/solutionSlice.js b/resources/js/slices/solutionSlice.js new file mode 100644 index 000000000..94e11dbc7 --- /dev/null +++ b/resources/js/slices/solutionSlice.js @@ -0,0 +1,36 @@ +/* eslint-disable no-param-reassign */ + +/* TODO: issue https://github.com/Hexlet/hexlet-sicp/issues/1526 + Упростить логику, убрав стейт из редакса и сделав локальный стейт в Teacher's Solution! +*/ +import { createSlice } from '@reduxjs/toolkit'; +import { handleNewCheckResult } from './checkResultSlice'; + +const solutionSlice = createSlice({ + name: 'showSolution', + initialState: { + startTime: Date.now(), + waitingTime: 1200000, + displaySolutionState: 'notShown', + }, + reducers: { + setStartTime(state, { payload }) { + state.startTime = payload.startTime; + }, + changeShowStatus(state, { payload }) { + state.displaySolutionState = payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(handleNewCheckResult, (state, action) => { + const { resultStatus } = action.payload; + console.log(resultStatus); + if (resultStatus === 'success') { + state.displaySolutionState = 'isShown'; + } + }); + }, +}); + +export const { setStartTime, changeShowStatus } = solutionSlice.actions; +export default solutionSlice.reducer;