diff --git a/services/app/apps/codebattle/assets/css/style.scss b/services/app/apps/codebattle/assets/css/style.scss index f971be63a..38230dd5b 100644 --- a/services/app/apps/codebattle/assets/css/style.scss +++ b/services/app/apps/codebattle/assets/css/style.scss @@ -687,6 +687,14 @@ a { opacity: 0; } +.cb-opacity-05 { + opacity: 0.05; +} + +.cb-opacity-10 { + opacity: 0.10; +} + .cb-opacity-25 { opacity: 0.25; } @@ -2095,12 +2103,12 @@ a.cb-text:hover { border-color: $cb-secondary-focus-border; } - &:hover { + &:hover:not(:disabled):not(.disabled) { background-color: $cb-secondary-hover-background; border-color: $cb-secondary-hover-border; } - &:focus { + &:focus:not(:disabled):not(.disabled) { background-color: $cb-secondary; border-color: $cb-secondary-focus-border; } @@ -2140,13 +2148,13 @@ a.cb-text:hover { border-color: $cb-secondary-focus-border; } - &:hover { + &:hover:not(:disabled):not(.disabled) { color: white; background-color: $cb-secondary-hover-background; border-color: $cb-secondary-hover-border; } - &:focus { + &:focus:not(:disabled):not(.disabled) { color: white; background-color: $cb-secondary; border-color: $cb-secondary-focus-border; @@ -2178,8 +2186,12 @@ a.cb-text:hover { background-color: $cb-success; border-color: $cb-success; - &:hover:not(:disabled), - &:hover:not(.disabled) { + &:hover:not(:disabled):not(.disabled) { + background-color: $cb-hovered-success; + border-color: $cb-hovered-success; + } + + &:focus:not(:disabled):not(.disabled) { background-color: $cb-hovered-success; border-color: $cb-hovered-success; } @@ -2188,8 +2200,7 @@ a.cb-text:hover { .cb-btn-outline-success { border-color: $cb-success; - &:hover:not(:disabled), - &:hover:not(.disabled) { + &:hover:not(:disabled):not(.disabled) { background-color: $cb-hovered-success; border-color: $cb-hovered-success; } diff --git a/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx b/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx index 949e6f63d..1c9dcb0f5 100644 --- a/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx +++ b/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx @@ -19,16 +19,18 @@ import waitingRoom from '../widgets/machines/waitingRoom'; import RootContainer from '../widgets/pages/RoomWidget'; import reducers from '../widgets/slices'; +jest.mock('pixelmatch', () => ({})); + jest.mock('monaco-editor', () => ({ editor: { - defineTheme: () => {}, + defineTheme: () => { }, create: () => ({ - dispose: () => {}, - onDidChangeModelContent: () => {}, - setValue: () => {}, - getValue: () => {}, - getModel: () => {}, - focus: () => {}, + dispose: () => { }, + onDidChangeModelContent: () => { }, + setValue: () => { }, + getValue: () => { }, + getModel: () => { }, + focus: () => { }, }), }, })); @@ -37,7 +39,7 @@ jest.mock('monaco-vim', () => ({ VimMode: class { constructor() { return { - dispose: () => {}, + dispose: () => { }, }; } }, @@ -242,38 +244,6 @@ test('test rendering preview game component', async () => { expect(await findByText(/Examples:/)).toBeInTheDocument(); }); -test('test game guide', async () => { - const store = configureStore({ - reducer, - preloadedState, - }); - - const { findByRole, user } = setup( - - - - - , - ); - - const showGuideButton = await findByRole('button', { name: 'Show guide' }); - - await user.click(showGuideButton); - - const closeGuideButton = await findByRole('button', { name: 'Close' }); - expect(closeGuideButton).toBeInTheDocument(); - - await user.click(closeGuideButton); - - expect(closeGuideButton).not.toBeInTheDocument(); -}); - test('test a bot invite button', async () => { const store = configureStore({ reducer, diff --git a/services/app/apps/codebattle/assets/js/__tests__/UserSettings.test.jsx b/services/app/apps/codebattle/assets/js/__tests__/UserSettings.test.jsx index 02960e1f2..6c04e40a2 100644 --- a/services/app/apps/codebattle/assets/js/__tests__/UserSettings.test.jsx +++ b/services/app/apps/codebattle/assets/js/__tests__/UserSettings.test.jsx @@ -76,19 +76,19 @@ describe('UserSettings test cases', () => { .spyOn(axios, 'patch') .mockResolvedValueOnce({ data: {} }); const { - getByRole, getByLabelText, getByTestId, user, -} = setup( - - - , + getByRole, getByLabelText, getByTestId, user, + } = setup( + + + , ); const submitButton = getByLabelText('SubmitForm'); const nameInput = getByTestId('nameInput'); - const langSelect = getByTestId('langSelect'); + const codeLangSelect = getByTestId('code-langSelect'); await user.clear(nameInput); await user.type(nameInput, 'Dmitry'); - await user.selectOptions(langSelect, 'Javascript'); + await user.selectOptions(codeLangSelect, 'Javascript'); await user.click(submitButton); await waitFor(() => { @@ -98,6 +98,10 @@ describe('UserSettings test cases', () => { clan: '', name: 'Dmitry', lang: 'js', + locale: undefined, + lang_view: 'code', + db_type: '', + style_lang: '', sound_settings: { level: 6, type: 'standard', @@ -111,11 +115,11 @@ describe('UserSettings test cases', () => { test('failed user settings update', async () => { const { - getByTestId, getByLabelText, findByRole, findByText, user, -} = setup( - - - , + getByTestId, getByLabelText, findByRole, findByText, user, + } = setup( + + + , ); const submitButton = getByLabelText('SubmitForm'); const nameInput = getByTestId('nameInput'); diff --git a/services/app/apps/codebattle/assets/js/i18n/dayjs.js b/services/app/apps/codebattle/assets/js/i18n/dayjs.js index b6274d5b6..181cf0887 100644 --- a/services/app/apps/codebattle/assets/js/i18n/dayjs.js +++ b/services/app/apps/codebattle/assets/js/i18n/dayjs.js @@ -1,10 +1,23 @@ import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import timezone from 'dayjs/plugin/timezone'; +import updateLocale from 'dayjs/plugin/updateLocale'; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(duration); +dayjs.extend(updateLocale); + +const locale = dayjs.locale(); + +// const locale = dayjs.locale('es'); +// dayjs.tz.setDefault('Europe/Madrid'); +console.log(`Local: ${dayjs.tz.guess()}`); +/* eslint-disable-next-line */ + +dayjs.updateLocale(locale, { + weekStart: 1, +}); export default dayjs; diff --git a/services/app/apps/codebattle/assets/js/iframes/cssbattle/index.js b/services/app/apps/codebattle/assets/js/iframes/cssbattle/index.js new file mode 100644 index 000000000..2912f6ef5 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/iframes/cssbattle/index.js @@ -0,0 +1,36 @@ +import { toPng } from 'html-to-image'; + +const messageType = 'cssbattle'; + +// if (event.origin.startsWith('https://codebattle.hexlet.io/games/')) { +window.addEventListener( + 'message', + event => { + try { + if (event.data.type !== 'cssbattle') { + return; + } + + window.parent.postMessage( + { type: messageType, data: event.data }, + event.origin, + ); + + if (event.data?.userId) { + const { bodyStr, userId } = event.data; + + document.body.innerHTML = bodyStr; + + toPng(document.body).then(dataUrl => { + window.parent.postMessage( + { type: messageType, dataUrl, userId }, + event.origin, + ); + }); + } + } catch (e) { + console.error(e.message); + } + }, + false, +); diff --git a/services/app/apps/codebattle/assets/js/socket.js b/services/app/apps/codebattle/assets/js/socket.js index f5beb62b5..14c913ffd 100644 --- a/services/app/apps/codebattle/assets/js/socket.js +++ b/services/app/apps/codebattle/assets/js/socket.js @@ -76,13 +76,14 @@ export const channelMethods = { gameScore: 'game:score', gameCancel: 'game:cancel', gameCreate: 'game:create', - cssGameCreate: 'game:css:create', + experimentGameCreate: 'game:experiment:create', gameCreateInvite: 'game:create_invite', gameAcceptInvite: 'game:accept_invite', gameDeclineInvite: 'game:decline_invite', gameCancelInvite: 'game:cancel_invite', reportOnPlayer: 'game:report', + gameTaskChangeTarget: 'game:task:change_target', chatAddMsg: 'chat:add_msg', chatCommand: 'chat:command', 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 0b61384a6..5ae9fe294 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import '../initEditor'; import languages from '../config/languages'; +import { actions } from '../slices'; import useEditor from '../utils/useEditor'; import EditorLoading from './EditorLoading'; @@ -105,6 +106,8 @@ Editor.propTypes = { editable: PropTypes.bool, roomMode: PropTypes.string.isRequired, checkResult: PropTypes.func.isRequired, + toggleMuteSound: PropTypes.func, + mute: PropTypes.bool, userType: PropTypes.string.isRequired, userId: PropTypes.number.isRequired, }; @@ -114,8 +117,10 @@ Editor.defaultProps = { lineNumbers: 'on', syntax: 'js', fontSize: 16, + mute: true, editable: false, loading: false, + toggleMuteSound: actions.toggleMuteSound, }; export default memo(Editor); diff --git a/services/app/apps/codebattle/assets/js/widgets/components/EditorGameBar.jsx b/services/app/apps/codebattle/assets/js/widgets/components/EditorGameBar.jsx new file mode 100644 index 000000000..b4854d889 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/components/EditorGameBar.jsx @@ -0,0 +1,43 @@ +import React, { memo } from 'react'; + +import cn from 'classnames'; +import { useSelector } from 'react-redux'; + +import { getPregressbarClass, getPregressbarWidth } from '@/pages/lobby/GameProgressBar'; + +import EditorThemeCodes from '../config/editorThemes'; +import * as selectors from '../selectors'; + +function EditorGameBar({ userId, theme }) { + const checkResult = useSelector(selectors.executionOutputSelector(userId)); + + const panelClassName = cn('d-flex position-absolute justify-content-center w-100', { + 'bg-white': theme === EditorThemeCodes.light, + 'bg-dark': theme === EditorThemeCodes.dark, + }); + const editorBar = cn( + 'cb-editor-game-progress-bar rounded-bottom bg-light border-top-0', + 'd-flex justify-content-center pb-2 pt-1 px-4', + { + 'bg-light': theme === EditorThemeCodes.light, + 'bg-dark': theme === EditorThemeCodes.dark, + }, + ); + + return ( +
+
+
+
+
+
+
+ ); +} + +export default memo(EditorGameBar); diff --git a/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx b/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx index 25e9ed976..a931e77e8 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/LanguageIcon.jsx @@ -9,7 +9,10 @@ import GolangOriginalIcon from 'react-devicons/go/original'; import HaskellOriginalIcon from 'react-devicons/haskell/original'; import JavaOriginalIcon from 'react-devicons/java/original'; import LessOriginalIcon from 'react-devicons/less/plain-wordmark'; +import MongodbOriginalIcon from 'react-devicons/mongodb/original'; +import MysqlOriginalIcon from 'react-devicons/mysql/original'; import NodejsPlainIcon from 'react-devicons/nodejs/plain'; +import PostgresqlOriginalIcon from 'react-devicons/postgresql/original'; import SassOriginalIcon from 'react-devicons/sass/original'; import StylusOriginalIcon from 'react-devicons/stylus/original'; import SwiftOriginalIcon from 'react-devicons/swift/original'; @@ -40,9 +43,15 @@ const iconRenderers = { rust: className => , ts: className => , css: className => , - stylus: className => , - less: className => , - sass: className => , + stylus: className => ( + + ), + less: className => , + sass: className => , typescript: className => , java: className => ( ), + postgresql: className => ( + + ), + mongodb: className => ( + + ), + mysql: className => ( + + ), default: () => null, }; diff --git a/services/app/apps/codebattle/assets/js/widgets/config/editor/mongodb.js b/services/app/apps/codebattle/assets/js/widgets/config/editor/mongodb.js new file mode 100644 index 000000000..ba1938f99 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/config/editor/mongodb.js @@ -0,0 +1,130 @@ +// import tagKeywords from './tagKeywords'; + +const comparisonOperators = [ + 'eq', 'gt', 'gte', 'lt', 'lte', 'ne', 'in', 'nin', 'exists', +]; + +const logicalOperators = [ + 'and', 'not', 'nor', 'or', +]; + +const mongoOperators = [ + 'accumulator', 'addToSet', 'avg', 'bottom', 'bottomN', 'covariancePop', + 'covarianceSamp', 'count', 'derivative', 'denseRank', 'documentNumber', + 'expMovingAvg', 'first', 'firstN', 'integral', 'last', 'lastN', 'max', + 'maxN', 'median', 'min', 'minN', 'percentile', 'push', 'rank', 'stdDevPop', + 'stdDevSamp', 'shift', 'sum', 'top', 'topN', 'locf', 'linearFill', + 'convert', 'ltrim', 'rtrim', 'toBool', 'toDate', 'toDecimal', 'toDouble', + 'toInt', 'toLong', 'toObjectId', 'toString', 'trim', 'abs', 'add', + 'allElementsTrue', 'anyElementTrue', 'arrayElemAt', 'arrayToObject', + 'binarySize', 'bsonSize', 'ceil', 'cmp', 'concat', 'concatArrays', 'cond', + 'dateAdd', 'dateDiff', 'dateFromParts', 'dateFromString', 'dateSubtract', + 'dateToParts', 'dateToString', 'dateTrunc', 'dayOfMonth', 'dayOfWeek', + 'dayOfYear', 'divide', 'exp', 'filter', 'floor', 'function', + 'getField', 'hour', 'ifNull', 'indexOfArray', + 'indexOfBytes', 'indexOfCP', 'isArray', 'isNumber', 'isoDayOfWeek', + 'isoWeek', 'isoWeekYear', 'let', 'literal', 'ln', 'log', 'log10', 'map', + 'mergeObjects', 'meta', 'millisecond', 'minute', 'mod', 'month', 'multiply', + 'objectToArray', 'pow', 'range', 'reduce', 'regexFind', + 'regexFindAll', 'regexMatch', 'replaceAll', 'replaceOne', 'reverseArray', + 'second', 'setDifference', 'setEquals', 'setIntersection', 'setIsSubset', + 'setUnion', 'size', 'slice', 'sortArray', 'split', 'sqrt', 'strcasecmp', + 'strLenBytes', 'strLenCP', 'substr', 'substrBytes', 'substrCP', 'subtract', + 'switch', 'toHashedIndexKey', 'toLower', 'toUpper', 'tsSecond', 'tsIncrement', + 'trunc', 'type', 'week', 'year', 'zip', 'bitAnd', 'bitOr', 'bitXor', + 'bitNot', 'all', 'bitsAllClear', 'bitsAllSet', 'bitsAnyClear', 'bitsAnySet', + 'comment', 'elemMatch', 'expr', 'geoIntersects', 'geoWithin', + 'jsonSchema', 'near', 'nearSphere', + 'regex', 'text', 'where', 'addFields', 'bucket', 'bucketAuto', + 'changeStream', 'collStats', 'currentOp', 'densify', 'documents', 'facet', + 'fill', 'geoNear', 'graphLookup', 'group', 'indexStats', 'limit', + 'listLocalSessions', 'lookup', 'match', 'merge', 'out', 'project', + 'redact', 'replaceRoot', 'replaceWith', 'sample', 'search', 'searchMeta', + 'set', 'setWindowFields', 'skip', 'sort', 'sortByCount', 'unionWith', + 'unset', 'unwind', 'vectorSearch', +]; + +export const languageConfig = { + comments: { + lineComment: '//', + blockComment: ['/*', '*/'], + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: "'", close: "'", notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] }, + ], + surroundingPairs: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ["'", "'"], + ['"', '"'], + ['`', '`'], + ], + folding: { + markers: { + start: /^\s*\/\/\s*#?region\b/, + end: /^\s*\/\/\s*#?endregion\b/, + }, + }, +}; + +export default { + defaultToken: '', + tagKeywords: [...mongoOperators, ...comparisonOperators, ...logicalOperators], + + tokenizer: { + root: [ + // Comments + [/\/\/.*$/, 'comment'], + [/\/\*/, 'comment', '@comment'], + + // Strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], + [/'([^'\\]|\\.)*$/, 'string.invalid'], + [/"/, 'string', '@string_double'], + [/'/, 'string', '@string_single'], + + // Numbers + [/-?\d*\.\d+([eE][-+]?\d+)?/, 'number.float'], + [/0[xX][0-9a-fA-F]+/, 'number.hex'], + [/-?\d+/, 'number'], + + // Brackets and delimiters + [/[{}()[]]/, '@brackets'], + [/[;,.]/, 'delimiter'], + + // Identifiers + [/[a-zA-Z_]\w*/, 'identifier'], + ], + + comment: [ + [/[^/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[/*]/, 'comment'], + ], + + string_double: [ + [/[^\\"]+/, 'string'], + [/\\./, 'string.escape'], + [/"/, 'string', '@pop'], + ], + + string_single: [ + [/[^\\']+/, 'string'], + [/\\./, 'string.escape'], + [/'/, 'string', '@pop'], + ], + }, + includeLF: false, +}; diff --git a/services/app/apps/codebattle/assets/js/widgets/config/languages.js b/services/app/apps/codebattle/assets/js/widgets/config/languages.js index 2f8316d12..5735d9bbc 100644 --- a/services/app/apps/codebattle/assets/js/widgets/config/languages.js +++ b/services/app/apps/codebattle/assets/js/widgets/config/languages.js @@ -17,11 +17,15 @@ const languages = { ts: 'typescript', css: 'css', less: 'less', - sass: 'scss', + sass: 'sass', stylus: 'stylus', + postgresql: 'postgresql', + mongodb: 'mongodb', + mysql: 'mysql', }; export const cssProcessors = ['css', 'less', 'sass', 'stylus']; +export const dbNames = ['postgresql', 'mysql', 'mongodb']; export const constructorLangauges = ['ruby']; diff --git a/services/app/apps/codebattle/assets/js/widgets/initEditor.js b/services/app/apps/codebattle/assets/js/widgets/initEditor.js index 832e66520..0008bd54c 100644 --- a/services/app/apps/codebattle/assets/js/widgets/initEditor.js +++ b/services/app/apps/codebattle/assets/js/widgets/initEditor.js @@ -2,6 +2,9 @@ import { loader } from '@monaco-editor/react'; import * as monacoLib from 'monaco-editor'; import haskellProvider from './config/editor/haskell'; +import mongodbProvider, { + languageConfig as mongodbLangConf, +} from './config/editor/mongodb'; import sassProvider from './config/editor/sass'; import stylusProvider from './config/editor/stylus'; @@ -23,4 +26,8 @@ loader.init().then(monaco => { monaco.languages.register({ id: 'scss', aliases: ['scss'] }); monaco.languages.setMonarchTokensProvider('scss', sassProvider); + + monaco.languages.register({ id: 'mongodb', aliases: ['mongodb'] }); + monaco.languages.setMonarchTokensProvider('mongodb', mongodbProvider); + monaco.languages.setLanguageConfiguration('mongodb', mongodbLangConf); }); diff --git a/services/app/apps/codebattle/assets/js/widgets/lib/cssbattle.js b/services/app/apps/codebattle/assets/js/widgets/lib/cssbattle.js new file mode 100644 index 000000000..13f9884b0 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/lib/cssbattle.js @@ -0,0 +1,132 @@ +import pixelmatch from 'pixelmatch'; + +const diffThreshold = 0.1; + +const getMatchPoints = (stats, width, height) => ( + 1 - stats / (width * height) +); + +const getMatchPercentageText = match => ( + `${(match * 100).toFixed(2)}%` +); + +export const matchBattlePictures = ( + firstImgDataUrl, + secondImgDataUrl, + targetImgDataUrl, + width, + height, +) => { + // 1: Prepare canvases for pixel matching + const firstImgCanvas = document.createElement('canvas'); + const secondImgCanvas = document.createElement('canvas'); + const targetImgCanvas = document.createElement('canvas'); + const firstDiffCanvas = document.createElement('canvas'); + const secondDiffCanvas = document.createElement('canvas'); + + firstImgCanvas.width = width; + firstImgCanvas.height = height; + secondImgCanvas.width = width; + secondImgCanvas.height = height; + targetImgCanvas.width = width; + targetImgCanvas.height = height; + firstDiffCanvas.width = width; + firstDiffCanvas.height = height; + secondDiffCanvas.width = width; + secondDiffCanvas.height = height; + + const firstContext = firstImgCanvas.getContext('2d'); + const secondContext = secondImgCanvas.getContext('2d'); + const targetContext = targetImgCanvas.getContext('2d'); + const firstDiffContext = firstDiffCanvas.getContext('2d'); + const secondDiffContext = secondDiffCanvas.getContext('2d'); + + // 2: Create images from data urls and draw on canvases + + const firstImg = new Image(width, height); + const secondImg = new Image(width, height); + const targetImg = new Image(width, height); + + firstImg.src = firstImgDataUrl; + secondImg.src = secondImgDataUrl; + targetImg.src = targetImgDataUrl; + + firstContext.drawImage(firstImg, 0, 0, width, height); + secondContext.drawImage(secondImg, 0, 0, width, height); + targetContext.drawImage(targetImg, 0, 0, width, height); + + // 3: Pixel match players images on target + + const firstDataImg = firstContext.getImageData(0, 0, width, height); + const secondDataImg = secondContext.getImageData(0, 0, width, height); + const targetDataImg = targetContext.getImageData(0, 0, width, height); + const firstDiff = firstDiffContext.createImageData(width, height); + const secondDiff = secondDiffContext.createImageData(width, height); + + const firstStats = pixelmatch( + firstDataImg.data, + targetDataImg.data, + firstDiff.data, + width, + height, + { threshold: diffThreshold }, + ); + const secondStats = pixelmatch( + secondDataImg.data, + targetDataImg.data, + secondDiff.data, + width, + height, + { threshold: diffThreshold }, + ); + + // 4: Return match statistics + // 4.1: Get match points + + const firstMatchPoints = getMatchPoints(firstStats, width, height); + const secondMatchPoints = getMatchPoints(secondStats, width, height); + + // 4.2: Retrive image data urls from diff canvases + firstDiffContext.putImageData(firstDiff, 0, 0); + secondDiffContext.putImageData(secondDiff, 0, 0); + firstDiffContext.save(); + secondDiffContext.save(); + + const firstDiffDataUrl = firstDiffCanvas.toDataURL('image/png'); + const secondDiffDataUrl = secondDiffCanvas.toDataURL('image/png'); + + // 4.3: Return final result + + const result = [ + { + match: firstMatchPoints, + matchPercentage: getMatchPercentageText(firstMatchPoints), + success: firstMatchPoints >= 0.9999, + diffDataUrl: firstDiffDataUrl, + + // for debug + imageCanvas: firstImgCanvas, + diffCanvas: firstDiffCanvas, + }, + { + match: secondMatchPoints, + matchPercentage: getMatchPercentageText(secondMatchPoints), + success: secondMatchPoints >= 0.9999, + diffDataUrl: secondDiffDataUrl, + + // for debug + imageCanvas: secondImgCanvas, + diffCanvas: secondDiffCanvas, + }, + + ]; + + return { + result, + + // for debug + targetCanvas: targetImgCanvas, + }; +}; + +export default matchBattlePictures; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js index e6926b128..4fea68dbd 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Channel.js @@ -205,6 +205,8 @@ export default class Channel { decamelizeKeys(params, { separator: '_' }), ); + pushInstance.receive('error', console.error); + return pushInstance; } diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js index e54618989..f8a72f3d5 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Chat.js @@ -58,12 +58,10 @@ export const connectToChat = (useChat = true, chatPage = 'channel', chatId) => d export const addMessage = payload => { channel - .push(channelMethods.chatAddMsg, payload) - .receive('error', error => console.error(error)); + .push(channelMethods.chatAddMsg, payload); }; export const pushCommand = command => { channel - .push(channelMethods.chatCommand, command) - .receive('error', error => console.error(error)); + .push(channelMethods.chatCommand, command); }; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js index ac8ff0121..d1d3605ca 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Lobby.js @@ -87,42 +87,36 @@ export const openDirect = (userId, name) => dispatch => { export const cancelGame = gameId => () => { channel - .push(channelMethods.gameCancel, { gameId }) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCancel, { gameId }); }; export const createGame = params => { channel - .push(channelMethods.gameCreate, params) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCreate, params); }; -export const createCssGame = params => { +export const createExperimentGame = params => { channel - .push(channelMethods.cssGameCreate, params) + .push(channelMethods.experimentGameCreate, params) .receive('error', error => console.error(error)); }; export const createInvite = invite => { channel - .push(channelMethods.gameCreateInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCreateInvite, invite); }; export const acceptInvite = invite => () => { channel - .push(channelMethods.gameAcceptInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameAcceptInvite, invite); }; export const declineInvite = invite => () => { channel - .push(channelMethods.gameDeclineInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameDeclineInvite, invite); }; export const cancelInvite = invite => () => { channel - .push(channelMethods.gameCancelInvite, invite) - .receive('error', error => console.error(error)); + .push(channelMethods.gameCancelInvite, invite); }; 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 010621a58..994bc9a79 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js @@ -237,8 +237,7 @@ export const sendEditorText = (editorText, langSlug = null) => (_dispatch, getSt // TODO: only for show tournament export const startRoundTournament = () => { channel - .push('tournament:start_round', {}) - .receive('error', error => console.error(error)); + .push('tournament:start_round', {}); }; export const sendEditorCursorPosition = offset => { @@ -1265,3 +1264,10 @@ export const updateGameHistoryState = nextRecordId => (dispatch, getState) => { break; } }; + +export const changeTaskImgDataUrl = imgDataUrl => () => { + channel.push(channelMethods.gameTaskChangeTarget, { imgDataUrl }) + .receive('ok', () => { + window.location.reload(); + }); +}; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js index bd16ec174..54ba90b9b 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Tournament.js @@ -194,8 +194,7 @@ export const uploadPlayers = playerIds => (dispatch, getState) => { .push('tournament:players:request', { playerIds }) .receive('ok', response => { dispatch(actions.updateTournamentPlayers(response.players)); - }) - .receive('error', error => console.error(error)); + }); } else { const playerIdsStr = playerIds.join(','); @@ -219,8 +218,7 @@ export const requestMatchesByPlayerId = userId => dispatch => { .receive('ok', data => { dispatch(actions.updateTournamentMatches(data.matches)); dispatch(actions.updateTournamentPlayers(data.players)); - }) - .receive('error', error => console.error(error)); + }); }; export const uploadPlayersMatches = playerId => dispatch => { @@ -230,13 +228,11 @@ export const uploadPlayersMatches = playerId => dispatch => { export const joinTournament = teamId => { const params = teamId !== undefined ? { teamId } : {}; channel - .push('tournament:join', params) - .receive('error', error => console.error(error)); + .push('tournament:join', params); }; export const leaveTournament = teamId => { const params = teamId !== undefined ? { teamId } : {}; channel - .push('tournament:leave', params) - .receive('error', error => console.error(error)); + .push('tournament:leave', params); }; diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js index 4313a3c65..ff0f9c648 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/TournamentAdmin.js @@ -171,8 +171,7 @@ export const uploadPlayers = playerIds => (dispatch, getState) => { .push('tournament:players:request', { playerIds }) .receive('ok', response => { dispatch(actions.updateTournamentPlayers(response.players)); - }) - .receive('error', error => console.error(error)); + }); } else { const playerIdsStr = playerIds.join(','); @@ -205,8 +204,7 @@ export const requestMatchesByPlayerId = userId => dispatch => { .receive('ok', data => { dispatch(actions.updateTournamentMatches(data.matches)); dispatch(actions.updateTournamentPlayers(data.players)); - }) - .receive('error', error => console.error(error)); + }); }; export const uploadPlayersMatches = playerId => (dispatch, getState) => { @@ -233,14 +231,12 @@ export const uploadPlayersMatches = playerId => (dispatch, getState) => { export const createCustomRound = params => { channel - .push('tournament:start_round', params) - .receive('error', error => console.error(error)); + .push('tournament:start_round', params); }; export const startTournament = () => { channel - .push('tournament:start', {}) - .receive('error', error => console.error(error)); + .push('tournament:start', {}); }; export const cancelTournament = () => dispatch => { @@ -248,57 +244,48 @@ export const cancelTournament = () => dispatch => { .push('tournament:cancel', {}) .receive('ok', response => { dispatch(actions.updateTournamentData(response.tournament)); - }) - .receive('error', error => console.error(error)); + }); }; export const restartTournament = () => { channel - .push('tournament:restart', {}) - .receive('error', error => console.error(error)); + .push('tournament:restart', {}); }; export const startRoundTournament = () => { channel - .push('tournament:start_round', {}) - .receive('error', error => console.error(error)); + .push('tournament:start_round', {}); }; export const finishRoundTournament = () => { channel - .push('tournament:finish_round', {}) - .receive('error', error => console.error(error)); + .push('tournament:finish_round', {}); }; export const toggleVisibleGameResult = gameId => { channel - .push('tournament:toggle_match_visible', { gameId }) - .receive('error', error => console.error(error)); + .push('tournament:toggle_match_visible', { gameId }); }; export const openUpTournament = () => { channel - .push('tournament:open_up', {}) - .receive('error', error => console.error(error)); + .push('tournament:open_up', {}); }; export const showTournamentResults = () => { channel - .push('tournament:toggle_show_results', {}) - .receive('error', error => console.error(error)); + .push('tournament:toggle_show_results', {}); }; export const sendMatchGameOver = matchId => { channel - .push('tournament:match:game_over', { matchId }) - .receive('error', error => console.error(error)); + .push('tournament:match:game_over', { matchId }); }; export const toggleBanUser = (userId, isBanned) => dispatch => { channel .push('tournament:ban:player', { userId }) - .receive('ok', () => dispatch(actions.updateTournamentPlayers([{ id: userId, isBanned }]))) - .receive('error', error => console.error(error)); + .receive('ok', () => dispatch(actions.updateTournamentPlayers([{ id: userId, isBanned }]))); }; export const sendNewReportState = (reportId, state) => dispatch => { diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js index 60baa0a5f..f16daa7b2 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/WaitingRoom.js @@ -134,20 +134,17 @@ export const addWaitingRoomListeners = ( export const pauseWaitingRoomMatchmaking = () => () => { channel - .push(channelMethods.matchmakingPause, {}) - .receive('error', error => console.error(error)); + .push(channelMethods.matchmakingPause, {}); }; export const startWaitingRoomMatchmaking = () => () => { channel - .push(channelMethods.matchmakingResume, {}) - .receive('error', error => console.error(error)); + .push(channelMethods.matchmakingResume, {}); }; export const restartWaitingRoomMatchmaking = () => () => { channel - .push(channelMethods.matchmakingRestart, {}) - .receive('error', error => console.error(error)); + .push(channelMethods.matchmakingRestart, {}); }; export default addWaitingRoomListeners; 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 34db8e9a4..f3a677cdb 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/RoomWidget.jsx @@ -6,7 +6,7 @@ import { CSSTransition, SwitchTransition } from 'react-transition-group'; import FeedbackAlertNotification from '../components/FeedbackAlertNotification'; import FeedbackWidget from '../components/FeedbackWidget'; -import GameWidgetGuide from '../components/GameWidgetGuide'; +// import GameWidgetGuide from '../components/GameWidgetGuide'; import RoomContext from '../components/RoomContext'; import * as machineSelectors from '../machines/selectors'; import useGameRoomMachine from '../utils/useGameRoomMachine'; @@ -47,7 +47,7 @@ function RoomWidget({ const mute = useGameRoomSoundSettings(); const { - tournamentId, + // tournamentId, viewMode, showWaitingRoom, showBattleRoom, @@ -80,7 +80,7 @@ function RoomWidget({ >
- + {/* */}
- +
- <> - - {openedReplayer ? ( + + {openedReplayer + ? ( )} - {showChatInput && ( - - )} - + {showChatInput && }
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/CssBattleInfoPanel.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/CssBattleInfoPanel.jsx new file mode 100644 index 000000000..329fa8145 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/CssBattleInfoPanel.jsx @@ -0,0 +1,166 @@ +import React, { + memo, + useCallback, +} from 'react'; + +import cn from 'classnames'; +import { compress } from 'lz-string'; +import { useDispatch } from 'react-redux'; + +import Loading from '@/components/Loading'; +import * as roomActions from '@/middlewares/Room'; +import useCssBattle from '@/utils/useCssBattle'; +import useHover from '@/utils/useHover'; + +const frameStyle = { + minWidth: 300, minHeight: 200, +}; + +const statusTitleMap = { + targetIsEmpty: 'Create a new css task', + targetIsInvalid: 'Error', + process: 'Processing...', + loading: 'Loading...', +}; + +function CssBattleInfoPanel() { + const dispatch = useDispatch(); + + const [ref, hovered] = useHover(); + + const { + matchStats, + leftImgRef, + rightImgRef, + targetImgRef, + leftSolutionIframe, + rightSolutionIframe, + handleLoadLeftIframe, + handleLoadRightIframe, + } = useCssBattle(); + + const handleClick = useCallback(() => { + const compressedDataUrl = compress(leftImgRef.current?.src); + dispatch(roomActions.changeTaskImgDataUrl(compressedDataUrl)); + }, [leftImgRef, dispatch]); + + const isLoading = matchStats.status === 'loading'; + const showStats = hovered && (matchStats.result[0]?.success && matchStats.status !== 'process'); + const showTargetControls = ['targetIsEmpty', 'targetIsInvalid'].includes(matchStats.status); + + return ( +
+
+
+
+ {isLoading && ( +
+ +
+ )} +
+ + +