Skip to content
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
6 changes: 4 additions & 2 deletions apps/codebattle/assets/css/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -451,8 +451,9 @@

.cb-profile-heatmap-grid {
width: 100%;
overflow: hidden;
padding: 0 14px 0.25rem 6px;
overflow-x: auto;
overflow-y: visible;
padding: 0.75rem 14px 0.25rem 6px;
}

.cb-profile-heatmap-grid svg {
Expand All @@ -461,6 +462,7 @@
min-width: 0;
max-width: 100%;
height: auto;
overflow: visible;
}

.cb-profile-heatmap-overlay {
Expand Down
54 changes: 49 additions & 5 deletions apps/codebattle/assets/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,15 @@ main > #app {
min-width: 250px;
}

.cb-game > [class*='col-'] {
min-width: 0;
}

.cb-game-chat-layout,
.cb-game-chat-container {
min-width: 0;
}

@media screen and (min-width: $md) {
.main-nav {
padding-left: clamp(0.25rem, 1vw, 0.5rem);
Expand Down Expand Up @@ -1394,6 +1403,18 @@ main > #app {

.cb-game-control-container {
width: 100%;
min-width: 0;
flex-shrink: 0;
}

.cb-game-chat-layout {
flex-direction: column;
}

.cb-game-chat-container {
width: 100%;
min-height: 0;
height: auto !important;
}

.main-nav {
Expand Down Expand Up @@ -1614,19 +1635,32 @@ $cb-bronze-bottom: #4a3223;
.dropdown-menu,
.cb-dropdown-menu {
& .dropdown-item.cb-dropdown-item {
color: white;
color: white !important;

&:hover:not(:active):not(.active) {
&:hover:not(:active):not(.active),
&:focus:not(:active):not(.active) {
color: white !important;
background-color: $cb-bg-panel;
}

&:active,
&.active {
color: white !important;
background-color: $cb-bg-panel;
}
}
}

.cb-dark-select,
.cb-dark-select option {
color: white;
background-color: $cb-bg-panel;
}

.cb-dark-select {
color-scheme: dark;
}

div.cb-dropdown-menu {
transform: translate3d(-100%, 0, 0);
}
Expand Down Expand Up @@ -1734,9 +1768,19 @@ div.cb-dropdown-menu {
}

@media screen and (min-width: $md) {
.cb-join-game-modal .modal-dialog {
min-width: 900px;
/* New width for default modal */
.modal-dialog.cb-join-game-modal {
width: calc(100vw - 2rem);
min-width: 0;
max-width: 900px;
}

.cb-join-game-modal table {
width: 100%;
table-layout: fixed;
}

.cb-join-game-modal .cb-username-td {
max-width: 0;
}
}

Expand Down
12 changes: 12 additions & 0 deletions apps/codebattle/assets/js/__tests__/ChatInputEmoji.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getEmojiSearchQuery } from '../widgets/components/ChatInput';

describe('chat emoticon search', () => {
test('maps happy and sad text emoticons to the matching emoji', () => {
expect(getEmojiSearchQuery(':)')).toBe('smiley');
expect(getEmojiSearchQuery(':(')).toBe('disappointed');
});

test('keeps regular emoji shortcodes unchanged', () => {
expect(getEmojiSearchQuery(':rocket')).toBe('rocket');
});
});
20 changes: 20 additions & 0 deletions apps/codebattle/assets/js/__tests__/Registration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('sign up', () => {
});

beforeEach(() => {
window.history.pushState({}, '', '/users/new');
fetchMock = vi.fn();
globalThis.fetch = fetchMock as unknown as typeof fetch;
});
Expand Down Expand Up @@ -85,4 +86,23 @@ describe('sign up', () => {
});
});
});

test('shows a readable password recovery confirmation', async () => {
window.history.pushState({}, '', '/remind_password');
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({}),
});

const { getByLabelText, findByText, user } = setup(<Registration />);

await user.type(getByLabelText('email'), 'user@example.com');
await user.click(getByLabelText('SubmitForm'));

const confirmation = await findByText(
'We have sent you an email with instructions on how to reset your password',
);

expect(confirmation).toHaveClass('text-white');
});
});
65 changes: 65 additions & 0 deletions apps/codebattle/assets/js/__tests__/Sound.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import sound, { configureSound } from '../widgets/lib/sound';

const howlerMocks = vi.hoisted(() => ({
howl: vi.fn(),
play: vi.fn(),
stop: vi.fn(),
volume: vi.fn(),
}));

vi.mock('howler', () => ({
Howl: function Howl(options: unknown) {
howlerMocks.howl(options);

return {
play: howlerMocks.play,
volume: howlerMocks.volume,
};
},
Howler: {
stop: howlerMocks.stop,
volume: howlerMocks.volume,
},
}));

vi.mock('@/inertia/pageProps', () => ({
getPageProp: () => ({
sound_settings: {
type: 'standard',
level: 5,
},
}),
}));

describe('game sound settings', () => {
beforeEach(() => {
localStorage.clear();
howlerMocks.howl.mockClear();
howlerMocks.play.mockClear();
howlerMocks.volume.mockClear();
});

test('uses newly saved sound settings without a page reload', () => {
configureSound({ type: 'cs', level: 7, tournamentLevel: 3 });

sound.play('win');

const options = howlerMocks.howl.mock.calls[0][0] as {
src: string;
volume: number;
};

expect(options.src).toBe('/assets/audio/audioSprites/csSpritesAudio.wav');
expect(options.volume).toBeCloseTo(0.7);
expect(howlerMocks.play).toHaveBeenCalledWith('win');
});

test('does not create a player for silent mode', () => {
configureSound({ type: 'silent', level: 5 });

sound.play('win');

expect(howlerMocks.howl).not.toHaveBeenCalled();
expect(howlerMocks.play).not.toHaveBeenCalled();
});
});
16 changes: 13 additions & 3 deletions apps/codebattle/assets/js/widgets/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,21 @@ const trimColons = (message: string) => message.slice(0, message.lastIndexOf(':'

const getColons = (message: string) => message.slice(message.lastIndexOf(':') + 1);

const emoticonSearchQueries: Record<string, string> = {
')': 'smiley',
'(': 'disappointed',
};

export const getEmojiSearchQuery = (message: string) => {
const query = getColons(message);

return emoticonSearchQueries[query] || query;
};

const getTooltipVisibility = async (msg: string) => {
const endsWithEmojiCodeRegex = /.*:[a-zA-Z]{0,}([^ ])+$/;
if (!endsWithEmojiCodeRegex.test(msg)) return Promise.resolve(false);
const colons = getColons(msg);
return !isEmpty(await SearchIndex.search(colons));
return !isEmpty(await SearchIndex.search(getEmojiSearchQuery(msg)));
};

interface ChatInputProps {
Expand Down Expand Up @@ -210,7 +220,7 @@ export default function ChatInput({
)}
{isTooltipVisible && (
<EmojiToolTip
colons={getColons(text)}
query={getEmojiSearchQuery(text)}
handleSelect={handleSelectEmodji}
hide={hideTooltip}
/>
Expand Down
8 changes: 4 additions & 4 deletions apps/codebattle/assets/js/widgets/components/EmojiTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ interface EmojiItem {
}

interface EmojiTooltipProps {
colons: string;
query: string;
handleSelect: (emoji: EmojiItem) => void;
hide: () => void;
}

export default function EmojiTooltip({ colons, handleSelect, hide }: EmojiTooltipProps) {
export default function EmojiTooltip({ query, handleSelect, hide }: EmojiTooltipProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [emojis, setEmojis] = useState<EmojiItem[]>([]);

Expand All @@ -32,7 +32,7 @@ export default function EmojiTooltip({ colons, handleSelect, hide }: EmojiToolti

useEffect(() => {
const fetchEmojis = async () => {
const rawEmojis = await SearchIndex.search(colons);
const rawEmojis = await SearchIndex.search(query);
const preparedEmojis = rawEmojis.map((emoji: EmojiItem) => ({
...emoji,
native: emoji.skins[0].native,
Expand All @@ -42,7 +42,7 @@ export default function EmojiTooltip({ colons, handleSelect, hide }: EmojiToolti
};

fetchEmojis();
}, [colons]);
}, [query]);

const decreaseIndex = () => {
setActiveIndex((prevIndex) => {
Expand Down
46 changes: 29 additions & 17 deletions apps/codebattle/assets/js/widgets/lib/sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,30 @@ interface SoundSettings {
type: string;
level: number;
tournament_level?: number;
tournamentLevel?: number;
}

const soundSettings = getPageProp<{ sound_settings: SoundSettings }>('current_user', {
sound_settings: { type: 'standard', level: 5 },
const defaultSoundSettings: SoundSettings = { type: 'standard', level: 5 };
const initialSoundSettings = getPageProp<{ sound_settings: SoundSettings }>('current_user', {
sound_settings: defaultSoundSettings,
}).sound_settings;
const soundType = soundSettings.type;
const defaultSoundLevel = soundSettings.level * 0.1;
const tournamentSoundLevel = isUndefined(soundSettings.tournament_level)
? defaultSoundLevel
: soundSettings.tournament_level * 0.1;

const audio = (type: string = soundType, volume: number = defaultSoundLevel) =>
let currentSoundSettings = { ...defaultSoundSettings, ...initialSoundSettings };

const getSoundType = () => currentSoundSettings.type;
const getDefaultSoundLevel = () => currentSoundSettings.level * 0.1;
const getTournamentSoundLevel = () => {
const tournamentLevel =
currentSoundSettings.tournamentLevel ?? currentSoundSettings.tournament_level;

return isUndefined(tournamentLevel) ? getDefaultSoundLevel() : tournamentLevel * 0.1;
};

const configureSound = (settings: SoundSettings) => {
currentSoundSettings = { ...defaultSoundSettings, ...settings };
};

const audio = (type: string = getSoundType(), volume: number = getDefaultSoundLevel()) =>
new Howl({
src: audioPaths[type as keyof typeof audioPaths],
sprite: (audioConfigs[type as keyof typeof audioConfigs] as { sprite?: unknown })
Expand All @@ -48,7 +60,7 @@ const getAssetPlayer = (path: string) => {
if (!assetPlayers[path]) {
assetPlayers[path] = new Howl({
src: path,
volume: defaultSoundLevel,
volume: getDefaultSoundLevel(),
});
}

Expand All @@ -58,27 +70,27 @@ const getAssetPlayer = (path: string) => {
const sound = {
play: (type: string, soundLevel?: number) => {
const isMute = JSON.parse((localStorage.getItem('ui_mute_sound') || false) as string);
if (getSoundType() === 'silent' || isMute) return;
const soundEffect = audio();
if (soundType === 'silent' || isMute) return;
Howler.volume(isUndefined(soundLevel) ? defaultSoundLevel : soundLevel);
Howler.volume(isUndefined(soundLevel) ? getDefaultSoundLevel() : soundLevel);
soundEffect.play(type);
},
playAsset: (path: string, soundLevel?: number) => {
const isMute = JSON.parse((localStorage.getItem('ui_mute_sound') || false) as string);
if (soundType === 'silent' || isMute) return;
Howler.volume(isUndefined(soundLevel) ? defaultSoundLevel : soundLevel);
if (getSoundType() === 'silent' || isMute) return;
Howler.volume(isUndefined(soundLevel) ? getDefaultSoundLevel() : soundLevel);
getAssetPlayer(path).play();
},
playTournamentAsset: (path: string) => {
const isMute = JSON.parse((localStorage.getItem('ui_mute_sound') || false) as string);
if (soundType === 'silent' || isMute) return;
Howler.volume(tournamentSoundLevel);
if (getSoundType() === 'silent' || isMute) return;
Howler.volume(getTournamentSoundLevel());
const player = getAssetPlayer(path);
player.volume(1);
player.play();
},
stop: () => Howler.stop(),
toggle: (volume: number = defaultSoundLevel) => {
toggle: (volume: number = getDefaultSoundLevel()) => {
Howler.volume(volume);
},
};
Expand All @@ -98,5 +110,5 @@ const createPlayer = () => ({
stop: () => Howler.stop(),
});

export { createPlayer };
export { configureSound, createPlayer };
export default sound;
2 changes: 1 addition & 1 deletion apps/codebattle/assets/js/widgets/pages/RoomWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function RoomWidget({ pageName, mainMachine, taskMachine, editorMachine }: RoomW
invisible: !visible,
})}
>
<div className="row no-gutter cb-game px-1">
<div className="row no-gutters cb-game px-1">
{showBattleRoom && (
<>
<InfoWidget viewMode={viewMode} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function ChatWidget() {
inputRef={inputRef as React.RefObject<HTMLInputElement>}
request={menuRequest}
>
<div className="d-flex cb-bg-panel shadow-sm h-100 cb-rounded">
<div className="d-flex cb-game-chat-layout cb-bg-panel shadow-sm h-100 cb-rounded">
<div
className={cn(
'd-flex flex-column flex-grow-1 position-relative p-0 h-100 mh-100 rounded-left',
Expand Down
Loading
Loading