hexlet-codebattle/codebattle

View on GitHub
services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx

Summary

Maintainability
A
3 hrs
Test Coverage
import React, {
  useEffect,
  useContext,
  useCallback,
  useRef,
} from 'react';

import { useInterpret } from '@xstate/react';
import cn from 'classnames';
import noop from 'lodash/noop';
import { useDispatch, useSelector } from 'react-redux';

import RoomContext from '../../components/RoomContext';
import editorModes from '../../config/editorModes';
import { gameRoomEditorStyles } from '../../config/editorSettings';
import {
  editorBtnStatuses as EditorBtnStatuses,
  editorSettingsByUserType,
} from '../../config/editorSettingsByUserType';
import editorThemes from '../../config/editorThemes';
import editorUserTypes from '../../config/editorUserTypes';
import GameModeCodes from '../../config/gameModes';
import {
  editorStateSelector,
  inBuilderRoomSelector,
  inPreviewRoomSelector,
  isRestrictedContentSelector,
  inTestingRoomSelector,
  isGameActiveSelector,
  isGameOverSelector,
  openedReplayerSelector,
} from '../../machines/selectors';
import * as GameActions from '../../middlewares/Room';
import * as selectors from '../../selectors';
import { actions } from '../../slices';
import useMachineStateSelector from '../../utils/useMachineStateSelector';

import EditorToolbar from './EditorToolbar';

const restrictedText = '\n\n\n\t"Only for Premium subscribers"';

function EditorContainer({
  id,
  editorMachine,
  type,
  orientation,
  cardClassName,
  editorContainerClassName,
  theme,
  editorState,
  editorHeight,
  editorMode,
  children,
}) {
  const dispatch = useDispatch();

  const toolbarRef = useRef();

  const player = useSelector(selectors.gamePlayerSelector(id));
  const gameMode = useSelector(selectors.gameModeSelector);
  const { tournamentId } = useSelector(selectors.gameStatusSelector);
  const subscriptionType = useSelector(selectors.subscriptionTypeSelector);

  const currentUserId = useSelector(selectors.currentUserIdSelector);
  const currentEditorLangSlug = useSelector(selectors.userLangSelector(currentUserId));

  const updateEditorValue = useCallback(data => dispatch(GameActions.updateEditorText(data)), [dispatch]);
  const sendEditorValue = useCallback(data => dispatch(GameActions.sendEditorText(data)), [dispatch]);

  const { mainService } = useContext(RoomContext);
  const isPreview = useMachineStateSelector(mainService, inPreviewRoomSelector);
  const isRestricted = useMachineStateSelector(mainService, isRestrictedContentSelector);
  const inTestingRoom = useMachineStateSelector(mainService, inTestingRoomSelector);
  const inBuilderRoom = useMachineStateSelector(mainService, inBuilderRoomSelector);
  const isActiveGame = useMachineStateSelector(mainService, isGameActiveSelector);
  const isGameOver = useMachineStateSelector(mainService, isGameOverSelector);
  const openedReplayer = useMachineStateSelector(mainService, openedReplayerSelector);

  const isTournamentGame = !!tournamentId;

  const context = { userId: id, type, subscriptionType };

  const editorService = useInterpret(
    editorMachine,
    {
      context,
      devTools: true,
      id: `editor_${id}`,
      actions: {
        userSendSolution: ctx => {
          if (ctx.editorState === 'active') {
            dispatch(GameActions.checkGameSolution());
          }
        },
        handleTimeoutFailureChecking: ctx => {
          dispatch(actions.updateExecutionOutput({
            userId: ctx.userId,
            status: 'client_timeout',
            output: '',
            result: {},
            asserts: [],
          }));

          dispatch(actions.updateCheckStatus({ [ctx.userId]: false }));
        },
      },
    },
  );

  const editorCurrent = useMachineStateSelector(editorService, editorStateSelector);

  const checkActiveTaskSolution = useCallback(() => editorService.send('user_check_solution'), [editorService]);
  const checkTestTaskSolution = useCallback(() => dispatch(GameActions.checkTaskSolution(editorService)), [dispatch, editorService]);

  const checkResult = inTestingRoom
    ? checkTestTaskSolution
    : checkActiveTaskSolution;

  useEffect(() => {
    if (inTestingRoom) {
      editorService.send('load_testing_editor');

      return () => {};
    }

    const clearEditor = GameActions.connectToEditor(editorService, player?.isBanned)(dispatch);

    return clearEditor;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const isNeedHotKeys = editorCurrent.context.type === editorUserTypes.currentUser;

  useEffect(() => {
    /** @param {KeyboardEvent} e */
    const check = e => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
        e.preventDefault();
        checkResult();
      }
    };

    if (isNeedHotKeys) {
      window.addEventListener('keydown', check);

      return () => {
        window.removeEventListener('keydown', check);
      };
    }

    return () => { };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [inTestingRoom]);

  const userSettings = {
    type,
    ...editorSettingsByUserType[type],
    ...editorCurrent.context,
  };

  const actionBtnsProps = {
    checkResult,
    currentEditorLangSlug,
    ...userSettings,
    showGiveUpBtn: !isTournamentGame && !inTestingRoom,
    giveUpBtnStatus: isActiveGame
      ? userSettings.giveUpBtnStatus
      : EditorBtnStatuses.disabled,
  };

  const toolbarParams = {
    toolbarRef,
    mode: tournamentId ? GameModeCodes.tournament : gameMode,
    player,
    editor: editorState,
    status: editorCurrent.value,
    actionBtnsProps,
    ...userSettings,
  };

  const canChange = userSettings.type === editorUserTypes.currentUser && !openedReplayer;
  const editable = !openedReplayer && userSettings.editable && userSettings.editorState !== 'banned';
  const canSendCursor = canChange && !inTestingRoom && !inBuilderRoom;
  const updateEditor = editorCurrent.context.editorState === 'testing' ? updateEditorValue : sendEditorValue;
  const onChange = canChange ? updateEditor : noop();
  const onChangeCursorSelection = canSendCursor ? GameActions.sendEditorCursorSelection : undefined;
  const onChangeCursorPosition = canSendCursor ? GameActions.sendEditorCursorPosition : undefined;

  const editorParams = {
    userId: id,
    hidingPanelControls: false,
    userType: type,
    syntax: editorState?.currentLangSlug || 'js',
    onChange,
    onChangeCursorSelection,
    onChangeCursorPosition,
    checkResult,
    value: isRestricted ? restrictedText : editorState?.text,
    editorHeight,
    mode: editorMode || editorModes.default,
    isTournamentGame,
    theme,
    ...userSettings,
    editable,
    loading: isPreview || editorCurrent.value === 'loading',
  };

  const isWon = player?.result === 'won';

  const pannelBackground = cn(editorContainerClassName, {
    'bg-warning': editorCurrent.matches('checking'),
    'bg-winner': isGameOver && editorCurrent.matches('idle') && isWon,
  });

  const gameRoomEditorStylesVersion2 = { minHeight: `calc(100vh - 92px - ${toolbarRef.current?.clientHeight || 0}px)` };

  return (
    <div data-editor-state={editorCurrent.value} className={pannelBackground}>
      <div
        className={`${editorParams.theme === editorThemes.dark ? 'bg-dark ' : 'bg-white '}${cardClassName}`}
        style={orientation === 'side' ? gameRoomEditorStylesVersion2 : gameRoomEditorStyles}
        data-guide-id={orientation === 'left' ? 'LeftEditor' : ''}
      >
        <EditorToolbar
          {...toolbarParams}
          toolbarClassNames="btn-toolbar justify-content-between align-items-center m-1"
          editorSettingClassNames="btn-group align-items-center m-1"
          userInfoClassNames="btn-group align-items-center justify-content-end m-1"
        />
        {children({
          ...editorParams,
        })}
      </div>
    </div>
  );
}

export default EditorContainer;