diff --git a/services/app/apps/codebattle/.eslintrc.yml b/services/app/apps/codebattle/.eslintrc.yml index 890c23caf..e4328ea96 100644 --- a/services/app/apps/codebattle/.eslintrc.yml +++ b/services/app/apps/codebattle/.eslintrc.yml @@ -19,6 +19,7 @@ extends: rules: no-console: 0 react/prop-types: 0 + react/no-unused-prop-types: 0 import/no-unresolved: 0 import/no-extraneous-dependencies: - 2 diff --git a/services/app/apps/codebattle/assets/js/app.js b/services/app/apps/codebattle/assets/js/app.js index 2cee1a525..1c6bafeff 100644 --- a/services/app/apps/codebattle/assets/js/app.js +++ b/services/app/apps/codebattle/assets/js/app.js @@ -48,7 +48,6 @@ import { renderUserPage, renderUsersRating, } from './widgets'; -import renderExtensionPopup from './widgets/components/ExtensionPopup'; if (process.env.NODE_ENV === 'development') { inspect({ @@ -91,7 +90,6 @@ window.addEventListener('phx:page-loading-stop', _info => NProgress.done()); liveSocket.connect(); const builderWidgetRoot = document.getElementById('builder-widget-root'); -const extension = document.getElementById('extension'); const gameWidgetRoot = document.getElementById('game-widget-root'); const heatmapRoot = document.getElementById('heatmap-root'); const onlineRoot = document.getElementById('online-root'); @@ -107,10 +105,6 @@ const adminTournamentRoot = document.getElementById('tournament-admin-root'); const eventWidgetRoot = document.getElementById('event-widget'); const userPageRoot = document.getElementById('user-page-root'); -if (extension) { - renderExtensionPopup(extension); -} - if (gameWidgetRoot) { renderGameWidget(gameWidgetRoot); } diff --git a/services/app/apps/codebattle/assets/js/widgets/components/ChatUserInfo.jsx b/services/app/apps/codebattle/assets/js/widgets/components/ChatUserInfo.jsx new file mode 100644 index 000000000..5750e12f4 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/ChatUserInfo.jsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import useHover from '../utils/useHover'; + +import UserInfo from './UserInfo'; + +function ChatUserInfo({ user, displayMenu, className = '' }) { + const [ref, hovered] = useHover(); + + return ( +
+ +
+ ); +} + +export default ChatUserInfo; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx index c4ed36e98..4a7861145 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx @@ -1,6 +1,7 @@ import React, { memo } from 'react'; import MonacoEditor, { loader } from '@monaco-editor/react'; +import PropTypes from 'prop-types'; import haskellProvider from '../config/editor/haskell'; import sassProvider from '../config/editor/sass'; @@ -64,4 +65,33 @@ function Editor(props) { ); } +Editor.propTypes = { + value: PropTypes.string.isRequired, + syntax: PropTypes.string, + onChange: PropTypes.func.isRequired, + theme: PropTypes.string.isRequired, + loading: PropTypes.bool, + wordWrap: PropTypes.string, + lineNumbers: PropTypes.string, + fontSize: PropTypes.number, + editable: PropTypes.bool, + gameMode: PropTypes.string.isRequired, + checkResult: PropTypes.func.isRequired, + toggleMuteSound: PropTypes.func.isRequired, + mute: PropTypes.bool.isRequired, + userType: PropTypes.string.isRequired, + userId: PropTypes.string.isRequired, + onChangeCursorSelection: PropTypes.func.isRequired, + onChangeCursorPosition: PropTypes.func.isRequired, +}; + +Editor.defaultProps = { + wordWrap: 'off', + lineNumbers: 'on', + syntax: 'js', + fontSize: 16, + editable: false, + loading: false, +}; + export default memo(Editor); diff --git a/services/app/apps/codebattle/assets/js/widgets/components/ExtensionPopup.jsx b/services/app/apps/codebattle/assets/js/widgets/components/ExtensionPopup.jsx deleted file mode 100644 index 66d8bca7b..000000000 --- a/services/app/apps/codebattle/assets/js/widgets/components/ExtensionPopup.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useState } from 'react'; - -import Button from 'react-bootstrap/Button'; -import Modal from 'react-bootstrap/Modal'; -import { createRoot } from 'react-dom'; - -const isExtensionInstalled = info => new Promise(resolve => { - const img = new Image(); - img.src = `chrome-extension://${info.id}/${info.path}`; - img.onload = () => { - resolve(true); - }; - img.onerror = () => { - resolve(false); - }; -}); - -function ExtensionPopup() { - const [modalShowing, setModalShowing] = useState(true); - const handleHide = () => { setModalShowing(false); }; - - return ( - - - -

Do you know?

-
-
- -

- {'We have a '} - - chrome extension - - , would you like to install it? -

-
- - - - -
- ); -} - -export default domElement => { - const lastCheckExtension = window.localStorage.getItem('lastCheckExtension'); - const nowTime = Date.now(); - const threeDay = 1000 * 60 * 60 * 24 * 3; - const isExpired = Number(lastCheckExtension) + threeDay < nowTime; - if (window.chrome && isExpired) { - // TODO: move to env config extension id and icon path - const extensionInfo = { id: 'embfhnfkfobkdohleknckodkmhgmpdli', path: 'assets/128.png' }; - isExtensionInstalled(extensionInfo).then(isInstall => { - if (!isInstall) { - window.localStorage.setItem('lastCheckExtension', nowTime); - createRoot(domElement).render(); - } - }); - } -}; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/InfoMessage.jsx b/services/app/apps/codebattle/assets/js/widgets/components/InfoMessage.jsx new file mode 100644 index 000000000..b48380e4f --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/InfoMessage.jsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import moment from 'moment'; + +function InfoMessage({ text, time }) { + return ( +
+ {text} + + {time ? moment.unix(time).format('HH:mm:ss') : ''} + +
+ ); +} + +export default InfoMessage; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/Loading.jsx b/services/app/apps/codebattle/assets/js/widgets/components/Loading.jsx index 63e1dc273..22b392971 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/Loading.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/Loading.jsx @@ -11,14 +11,14 @@ const getSize = ({ small = false, large = false, adaptive = false }) => { } }; -const Loading = params => { - const size = getSize(params); +function Loading(props) { + const size = getSize(props); return (
); -}; +} export default Loading; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/Message.jsx b/services/app/apps/codebattle/assets/js/widgets/components/Message.jsx index 3a9e5bcc4..97c4186eb 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/Message.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/Message.jsx @@ -1,23 +1,53 @@ import React from 'react'; import cn from 'classnames'; -import moment from 'moment'; +import useHover from '../utils/useHover'; + +import InfoMessage from './InfoMessage'; import MessageTag from './MessageTag'; import MessageTimestamp from './MessageTimestamp'; +import SystemMessage from './SystemMessage'; + +function MessageHeader({ name, time, hovered }) { + const playerClassName = cn( + 'd-inline-block text-truncate align-top text-nowrap cb-username-max-length mr-1', + { 'text-primary': hovered }, + ); + + return ( + <> + + + {name} + + + + + ); +} + +function MessagePart({ part, index, name }) { + if (part.slice(1) === name) { + return ( + + {part} + + ); + } -const MessageHeader = ({ name, time }) => ( - <> - - - {name} + if (part.startsWith('@')) { + return ( + + {part} - - - -); + ); + } + + return part; +} -const Message = ({ +function Message({ text = '', name = '', userId, @@ -25,84 +55,58 @@ const Message = ({ time, meta, displayMenu, -}) => { +}) { + const [chatHeaderRef, hoveredChatHeader] = useHover(); + if (!text) { return null; } if (type === 'system') { - const statusClassName = cn('text-small', { - 'text-danger': ['error', 'failure'].includes(meta?.status), - 'text-success': meta?.status === 'success', - 'text-muted': meta?.status === 'event', - }); - - return ( -
- {text} -
- ); + return ; } if (type === 'info') { - return ( -
- {text} - - {time ? moment.unix(time).format('HH:mm:ss') : ''} - -
- ); + return ; } const parts = text.split(/(@+[-a-zA-Z0-9_]+\b)/g); - const renderMessagePart = (part, i) => { - if (part.slice(1) === name) { - return ( - - {part} - - ); - } - if (part.startsWith('@')) { - return ( - - {part} - - ); - } - return part; - }; - const textPartsClassNames = cn('text-break', { 'cb-private-text': meta?.type === 'private', }); return (
- + - + - {parts.map((part, i) => renderMessagePart(part, i))} + {parts.map( + (part, i) => ( + /* eslint-disable react/no-array-index-key */ + + ), + )}
); -}; +} export default Message; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/Messages.jsx b/services/app/apps/codebattle/assets/js/widgets/components/Messages.jsx index 7e2d70505..1608512b9 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/Messages.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/Messages.jsx @@ -6,12 +6,12 @@ import useStayScrolled from '../utils/useStayScrolled'; import Message from './Message'; -const getKey = (id, time, name) => { +const getKey = (id, time, name, index) => { if (!time || !name) { return id; } - return `${id}-${time}-${name}`; + return `${id}-${time}-${name}-${index}`; }; function Messages({ messages, displayMenu = () => {}, disabled = false }) { @@ -41,12 +41,12 @@ function Messages({ messages, displayMenu = () => {}, disabled = false }) { ref={listRef} className="overflow-auto pt-0 pl-3 pr-2 position-relative cb-messages-list flex-grow-1" > - {messages.map(message => { + {messages.map((message, index) => { const { id, userId, name, text, type, time, meta, } = message; - const key = getKey(id, time, name); + const key = getKey(id, time, name, messages.length - index); return ( + {text} + + ); +} + +export default SystemMessage; diff --git a/services/app/apps/codebattle/assets/js/widgets/components/UserInfo.jsx b/services/app/apps/codebattle/assets/js/widgets/components/UserInfo.jsx index 619dcf38d..86d130c15 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/UserInfo.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/UserInfo.jsx @@ -48,6 +48,7 @@ function UserInfo({ className, user, lang, + hovered = false, hideLink = false, hideInfo = false, truncate = false, @@ -56,6 +57,7 @@ function UserInfo({ placement = Placements.bottomStart, }) { const { presenceList } = useSelector(selectors.lobbyDataSelector); + const isAdmin = useSelector(selectors.userIsAdminSelector(user?.id)); const content = useMemo(() => , [user]); if (!user?.id) { @@ -75,9 +77,11 @@ function UserInfo({ return ( { const commonClassName = 'd-flex align-items-center'; const onlineIndicatorClassName = cn('mr-1', { @@ -15,11 +15,11 @@ const UserName = ({ 'cb-user-offline': !isOnline, }); const userClassName = cn('text-truncate', { - 'text-danger': user.isAdmin, 'x-username-truncated': truncate, }); - - const userName = user.rank ? `${user.name}(${user.rank})` : user.name; + const userNameClassName = cn({ + 'text-primary': hovered, + }); return (
@@ -27,9 +27,15 @@ const UserName = ({ {user.isBot && } {hideLink ? ( - {userName} + + {user.name} + {user.rank && {`(${user.rank})`}} + ) : ( - {userName} + + {user.name} + {user.rank && {`(${user.rank})`}} + )}
); diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js index fbd57e997..5db8e33d4 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js @@ -213,36 +213,21 @@ export const updateEditorText = (editorText, langSlug = null) => (dispatch, getS ); }; -export const sendEditorLang = langSlug => (dispatch, getState) => { +export const sendEditorLang = langSlug => (_dispatch, getState) => { const state = getState(); const userId = selectors.currentUserIdSelector(state); const currentLangSlug = langSlug || selectors.userLangSelector(userId)(state); - dispatch( - actions.updateEditorText({ - userId, - langSlug: currentLangSlug, - }), - ); - channel.push(channelMethods.editorLang, { langSlug: currentLangSlug, }); }; -export const sendEditorText = (editorText, langSlug = null) => (dispatch, getState) => { +export const sendEditorText = (editorText, langSlug = null) => (_dispatch, getState) => { const state = getState(); const userId = selectors.currentUserIdSelector(state); const currentLangSlug = langSlug || selectors.userLangSelector(userId)(state); - dispatch( - actions.updateEditorText({ - userId, - editorText, - langSlug: currentLangSlug, - }), - ); - channel.push(channelMethods.editorData, { editorText, langSlug: currentLangSlug, @@ -309,6 +294,18 @@ export const sendCurrentLangAndSetTemplate = langSlug => (dispatch, getState) => const currentText = selectors.currentPlayerTextByLangSelector(langSlug)(state); const { solutionTemplate: template } = find(langs, { slug: langSlug }); const textToSet = currentText || template; + + const userId = selectors.currentUserIdSelector(state); + const newLangSlug = langSlug || selectors.userLangSelector(userId)(state); + + dispatch( + actions.updateEditorText({ + userId, + editorText: textToSet, + langSlug: newLangSlug, + }), + ); + dispatch(sendEditorText(textToSet, langSlug)); dispatch(sendEditorLang(langSlug)); }; @@ -324,13 +321,14 @@ export const resetTextToTemplateAndSend = langSlug => (dispatch, getState) => { const state = getState(); const langs = selectors.editorLangsSelector(state) || defaultLanguages; const { solutionTemplate: template } = find(langs, { slug: langSlug }); + dispatch(updateEditorText(template, langSlug)); dispatch(sendEditorText(template, langSlug)); }; export const soundNotification = notification(); export const addCursorListeners = (userId, onChangePosition, onChangeSelection) => { - if (!userId) { + if (!userId || isRecord) { return () => {}; } diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx index 1513df86b..82fe7d0c1 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx @@ -58,14 +58,14 @@ function PanelsSplitPane({ children, viewMode }) { return ( -
{children[0]}
+
{children[0]}
{children[1]}
); diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/ChatWidget.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/ChatWidget.jsx index 9991b6e7c..fb1939435 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/ChatWidget.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/ChatWidget.jsx @@ -1,20 +1,20 @@ import React, { useContext, - useMemo, + // useMemo, useRef, } from 'react'; import cn from 'classnames'; -import filter from 'lodash/filter'; -import uniqBy from 'lodash/uniqBy'; +// import filter from 'lodash/filter'; +// import uniqBy from 'lodash/uniqBy'; import { useSelector } from 'react-redux'; import ChatContextMenu from '../../components/ChatContextMenu'; import ChatHeader from '../../components/ChatHeader'; import ChatInput from '../../components/ChatInput'; +// import ChatUserInfo from '../../components/ChatUserInfo'; import Messages from '../../components/Messages'; import RoomContext from '../../components/RoomContext'; -import UserInfo from '../../components/UserInfo'; import GameRoomModes from '../../config/gameModes'; import { inTestingRoomSelector, @@ -43,10 +43,10 @@ function ChatWidget() { const isTestingRoom = useMachineStateSelector(mainService, inTestingRoomSelector); const isRestricted = useMachineStateSelector(mainService, isRestrictedContentSelector); - const isTournamentGame = (gameMode === GameRoomModes.tournament); + // const isTournamentGame = (gameMode === GameRoomModes.tournament); const isStandardGame = (gameMode === GameRoomModes.standard); const showChatInput = !openedReplayer && !isTestingRoom && useChat && !isRestricted; - const showChatParticipants = !isTestingRoom && useChat && !isRestricted; + // const showChatParticipants = !isTestingRoom && useChat && !isRestricted; const disabledChatHeader = isTestingRoom || !isOnline || !useChat; const disabledChatMessages = isTestingRoom || !useChat || isRestricted; @@ -62,10 +62,10 @@ function ChatWidget() { useChatRooms('page'); - const listOfUsers = useMemo(() => { - const uniqUsers = uniqBy(users, 'id'); - return isTournamentGame ? filter(uniqUsers, { isBot: false }) : uniqUsers; - }, [isTournamentGame, users]); + // const listOfUsers = useMemo(() => { + // const uniqUsers = uniqBy(users, 'id'); + // return isTournamentGame ? filter(uniqUsers, { isBot: false }) : uniqUsers; + // }, [isTournamentGame, users]); const activeRoom = useSelector(selectors.activeRoomSelector); const filteredMessages = messages.filter(message => shouldShowMessage(message, activeRoom)); @@ -104,29 +104,14 @@ function ChatWidget() {
- {showChatParticipants && ( -
-

- {`Online players: ${listOfUsers.length}`} -

- {listOfUsers.map(user => ( -
- -
- ))} -
- )} + {/* {showChatParticipants && ( */} + {/*
*/} + {/*

*/} + {/* {`Online players: ${listOfUsers.length}`} */} + {/*

*/} + {/* {listOfUsers.map(user => )} */} + {/*
*/} + {/* )} */} diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/CodebattlePlayer.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/CodebattlePlayer.jsx index b0e63674a..707bceaf0 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/CodebattlePlayer.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/CodebattlePlayer.jsx @@ -230,8 +230,10 @@ class CodebattlePlayer extends Component { onIntentEnd={this.onSliderHandleChangeIntentEnd} > ( -
-); +function SliderBar({ value, className }) { + return ( +
+ ); +} -const SliderAction = ({ - value, className, setGameState, event, -}) => ( -
- - {`Check started by ${event.userName}`} - +function SliderAction({ + value, + className, + event, + setGameState, +}) { + return ( +
+ + {`Check started by ${event.userName}`} + )} - > - ); +} -const SliderHandle = ({ value, className }) => ( -
-
-
-); +function SliderHandle({ value, className }) { + return ( +
+
+
+ ); +} -const CodebattleSliderBar = ({ - roomMachineState, handlerPosition, lastIntent, mainEvents, recordsCount, setGameState, -}) => ( - <> -
- { - roomMachineState.matches({ replayer: replayerMachineStates.holded }) - && ( - -) - } - +
+ +
+ {mainEvents.map(event => ( + + ))} + -
- {mainEvents.map(event => ( - - ))} - - -); + + ); +} export default CodebattleSliderBar; diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx index 294cdb8bf..ea3cbad5e 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx @@ -89,7 +89,10 @@ function EditorContainer({ 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 updateAndSendEditorValue = useCallback(data => { + dispatch(GameActions.updateEditorText(data)); + dispatch(GameActions.sendEditorText(data)); + }, [dispatch]); const { mainService } = useContext(RoomContext); const isPreview = useMachineStateSelector(mainService, inPreviewRoomSelector); @@ -194,7 +197,7 @@ function EditorContainer({ 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 updateEditor = editorCurrent.context.editorState === 'testing' ? updateEditorValue : updateAndSendEditorValue; const onChange = canChange ? updateEditor : noop; const onChangeCursorSelection = canSendCursor ? GameActions.sendEditorCursorSelection : noop; const onChangeCursorPosition = canSendCursor ? GameActions.sendEditorCursorPosition : noop; diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/GameActionButtons.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/GameActionButtons.jsx index 0dbbb5b3f..f343782e1 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/GameActionButtons.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/GameActionButtons.jsx @@ -2,6 +2,7 @@ import React, { useContext, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import i18next from 'i18next'; +import { Dropdown } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; import { useDispatch } from 'react-redux'; @@ -74,68 +75,54 @@ function CheckResultButton({ onClick, status }) { } } -function GiveUpButton({ onClick, status }) { - const dispatch = useDispatch(); +const CustomToggle = React.forwardRef(({ onClick, className, disabled }, ref) => ( + +)); + +function GiveUpButtonDropdownItem({ onSelect, status }) { const commonProps = { - type: 'button', - className: 'btn btn-outline-danger rounded-lg', + key: 'giveUp', + href: '#', title: i18next.t('Give Up'), - onClick, - 'data-toggle': 'tooltip', - 'data-placement': 'top', - 'data-guide-id': 'GiveUpButton', + onSelect, + disabled: status === 'disabled', }; - switch (status) { - case 'enabled': - return ( - - ); - case 'disabled': - return ( - - ); - default: { - dispatch(actions.setError(new Error('unnexpected give up status'))); - return null; - } - } + return ( + + + + Give up + + + ); } -function ResetButton({ onClick, status }) { - const dispatch = useDispatch(); +function ResetButtonDropDownItem({ onSelect, status }) { const commonProps = { - type: 'button', - className: 'btn btn-outline-secondary rounded-lg mx-1', + key: 'reset', + href: '#', title: i18next.t('Reset solution'), - onClick, - 'data-toggle': 'tooltip', - 'data-placement': 'top', - 'data-guide-id': 'ResetButton', + onSelect, + disabled: status === 'disabled', }; - switch (status) { - case 'enabled': - return ( - - ); - case 'disabled': - return ( - - ); - default: { - dispatch(actions.setError(new Error('unnexpected reset status'))); - return null; - } - } + return ( + + + + Reset Solution + + + ); } function GameActionButtons({ @@ -199,11 +186,23 @@ function GameActionButtons({ role="group" aria-label="Game actions" > - {showGiveUpBtn && ( - - )} - + + + + + + + + {showGiveUpBtn && } + + {renderModal()}
); diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/TaskAssignment.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/TaskAssignment.jsx index 06b393b09..468f524c8 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/TaskAssignment.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/TaskAssignment.jsx @@ -116,7 +116,7 @@ function TaskAssignment({ {!fullSize && (