Skip to content

Commit 6bdbdb6

Browse files
authored
Merge pull request #2330 from hexlet-codebattle/agent/fix-next-open-issues
Fix reported interface and sound issues
2 parents 3fed558 + 08a0237 commit 6bdbdb6

16 files changed

Lines changed: 211 additions & 38 deletions

File tree

apps/codebattle/assets/css/custom.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,9 @@
451451

452452
.cb-profile-heatmap-grid {
453453
width: 100%;
454-
overflow: hidden;
455-
padding: 0 14px 0.25rem 6px;
454+
overflow-x: auto;
455+
overflow-y: visible;
456+
padding: 0.75rem 14px 0.25rem 6px;
456457
}
457458

458459
.cb-profile-heatmap-grid svg {
@@ -461,6 +462,7 @@
461462
min-width: 0;
462463
max-width: 100%;
463464
height: auto;
465+
overflow: visible;
464466
}
465467

466468
.cb-profile-heatmap-overlay {

apps/codebattle/assets/css/style.scss

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,15 @@ main > #app {
13051305
min-width: 250px;
13061306
}
13071307

1308+
.cb-game > [class*='col-'] {
1309+
min-width: 0;
1310+
}
1311+
1312+
.cb-game-chat-layout,
1313+
.cb-game-chat-container {
1314+
min-width: 0;
1315+
}
1316+
13081317
@media screen and (min-width: $md) {
13091318
.main-nav {
13101319
padding-left: clamp(0.25rem, 1vw, 0.5rem);
@@ -1394,6 +1403,18 @@ main > #app {
13941403

13951404
.cb-game-control-container {
13961405
width: 100%;
1406+
min-width: 0;
1407+
flex-shrink: 0;
1408+
}
1409+
1410+
.cb-game-chat-layout {
1411+
flex-direction: column;
1412+
}
1413+
1414+
.cb-game-chat-container {
1415+
width: 100%;
1416+
min-height: 0;
1417+
height: auto !important;
13971418
}
13981419

13991420
.main-nav {
@@ -1614,19 +1635,32 @@ $cb-bronze-bottom: #4a3223;
16141635
.dropdown-menu,
16151636
.cb-dropdown-menu {
16161637
& .dropdown-item.cb-dropdown-item {
1617-
color: white;
1638+
color: white !important;
16181639

1619-
&:hover:not(:active):not(.active) {
1640+
&:hover:not(:active):not(.active),
1641+
&:focus:not(:active):not(.active) {
1642+
color: white !important;
16201643
background-color: $cb-bg-panel;
16211644
}
16221645

16231646
&:active,
16241647
&.active {
1648+
color: white !important;
16251649
background-color: $cb-bg-panel;
16261650
}
16271651
}
16281652
}
16291653

1654+
.cb-dark-select,
1655+
.cb-dark-select option {
1656+
color: white;
1657+
background-color: $cb-bg-panel;
1658+
}
1659+
1660+
.cb-dark-select {
1661+
color-scheme: dark;
1662+
}
1663+
16301664
div.cb-dropdown-menu {
16311665
transform: translate3d(-100%, 0, 0);
16321666
}
@@ -1734,9 +1768,19 @@ div.cb-dropdown-menu {
17341768
}
17351769

17361770
@media screen and (min-width: $md) {
1737-
.cb-join-game-modal .modal-dialog {
1738-
min-width: 900px;
1739-
/* New width for default modal */
1771+
.modal-dialog.cb-join-game-modal {
1772+
width: calc(100vw - 2rem);
1773+
min-width: 0;
1774+
max-width: 900px;
1775+
}
1776+
1777+
.cb-join-game-modal table {
1778+
width: 100%;
1779+
table-layout: fixed;
1780+
}
1781+
1782+
.cb-join-game-modal .cb-username-td {
1783+
max-width: 0;
17401784
}
17411785
}
17421786

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getEmojiSearchQuery } from '../widgets/components/ChatInput';
2+
3+
describe('chat emoticon search', () => {
4+
test('maps happy and sad text emoticons to the matching emoji', () => {
5+
expect(getEmojiSearchQuery(':)')).toBe('smiley');
6+
expect(getEmojiSearchQuery(':(')).toBe('disappointed');
7+
});
8+
9+
test('keeps regular emoji shortcodes unchanged', () => {
10+
expect(getEmojiSearchQuery(':rocket')).toBe('rocket');
11+
});
12+
});

apps/codebattle/assets/js/__tests__/Registration.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('sign up', () => {
3636
});
3737

3838
beforeEach(() => {
39+
window.history.pushState({}, '', '/users/new');
3940
fetchMock = vi.fn();
4041
globalThis.fetch = fetchMock as unknown as typeof fetch;
4142
});
@@ -85,4 +86,23 @@ describe('sign up', () => {
8586
});
8687
});
8788
});
89+
90+
test('shows a readable password recovery confirmation', async () => {
91+
window.history.pushState({}, '', '/remind_password');
92+
fetchMock.mockResolvedValueOnce({
93+
ok: true,
94+
json: async () => ({}),
95+
});
96+
97+
const { getByLabelText, findByText, user } = setup(<Registration />);
98+
99+
await user.type(getByLabelText('email'), 'user@example.com');
100+
await user.click(getByLabelText('SubmitForm'));
101+
102+
const confirmation = await findByText(
103+
'We have sent you an email with instructions on how to reset your password',
104+
);
105+
106+
expect(confirmation).toHaveClass('text-white');
107+
});
88108
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import sound, { configureSound } from '../widgets/lib/sound';
2+
3+
const howlerMocks = vi.hoisted(() => ({
4+
howl: vi.fn(),
5+
play: vi.fn(),
6+
stop: vi.fn(),
7+
volume: vi.fn(),
8+
}));
9+
10+
vi.mock('howler', () => ({
11+
Howl: function Howl(options: unknown) {
12+
howlerMocks.howl(options);
13+
14+
return {
15+
play: howlerMocks.play,
16+
volume: howlerMocks.volume,
17+
};
18+
},
19+
Howler: {
20+
stop: howlerMocks.stop,
21+
volume: howlerMocks.volume,
22+
},
23+
}));
24+
25+
vi.mock('@/inertia/pageProps', () => ({
26+
getPageProp: () => ({
27+
sound_settings: {
28+
type: 'standard',
29+
level: 5,
30+
},
31+
}),
32+
}));
33+
34+
describe('game sound settings', () => {
35+
beforeEach(() => {
36+
localStorage.clear();
37+
howlerMocks.howl.mockClear();
38+
howlerMocks.play.mockClear();
39+
howlerMocks.volume.mockClear();
40+
});
41+
42+
test('uses newly saved sound settings without a page reload', () => {
43+
configureSound({ type: 'cs', level: 7, tournamentLevel: 3 });
44+
45+
sound.play('win');
46+
47+
const options = howlerMocks.howl.mock.calls[0][0] as {
48+
src: string;
49+
volume: number;
50+
};
51+
52+
expect(options.src).toBe('/assets/audio/audioSprites/csSpritesAudio.wav');
53+
expect(options.volume).toBeCloseTo(0.7);
54+
expect(howlerMocks.play).toHaveBeenCalledWith('win');
55+
});
56+
57+
test('does not create a player for silent mode', () => {
58+
configureSound({ type: 'silent', level: 5 });
59+
60+
sound.play('win');
61+
62+
expect(howlerMocks.howl).not.toHaveBeenCalled();
63+
expect(howlerMocks.play).not.toHaveBeenCalled();
64+
});
65+
});

apps/codebattle/assets/js/widgets/components/ChatInput.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,21 @@ const trimColons = (message: string) => message.slice(0, message.lastIndexOf(':'
3434

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

37+
const emoticonSearchQueries: Record<string, string> = {
38+
')': 'smiley',
39+
'(': 'disappointed',
40+
};
41+
42+
export const getEmojiSearchQuery = (message: string) => {
43+
const query = getColons(message);
44+
45+
return emoticonSearchQueries[query] || query;
46+
};
47+
3748
const getTooltipVisibility = async (msg: string) => {
3849
const endsWithEmojiCodeRegex = /.*:[a-zA-Z]{0,}([^ ])+$/;
3950
if (!endsWithEmojiCodeRegex.test(msg)) return Promise.resolve(false);
40-
const colons = getColons(msg);
41-
return !isEmpty(await SearchIndex.search(colons));
51+
return !isEmpty(await SearchIndex.search(getEmojiSearchQuery(msg)));
4252
};
4353

4454
interface ChatInputProps {
@@ -210,7 +220,7 @@ export default function ChatInput({
210220
)}
211221
{isTooltipVisible && (
212222
<EmojiToolTip
213-
colons={getColons(text)}
223+
query={getEmojiSearchQuery(text)}
214224
handleSelect={handleSelectEmodji}
215225
hide={hideTooltip}
216226
/>

apps/codebattle/assets/js/widgets/components/EmojiTooltip.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ interface EmojiItem {
1414
}
1515

1616
interface EmojiTooltipProps {
17-
colons: string;
17+
query: string;
1818
handleSelect: (emoji: EmojiItem) => void;
1919
hide: () => void;
2020
}
2121

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

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

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

4444
fetchEmojis();
45-
}, [colons]);
45+
}, [query]);
4646

4747
const decreaseIndex = () => {
4848
setActiveIndex((prevIndex) => {

apps/codebattle/assets/js/widgets/lib/sound.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,30 @@ interface SoundSettings {
2424
type: string;
2525
level: number;
2626
tournament_level?: number;
27+
tournamentLevel?: number;
2728
}
2829

29-
const soundSettings = getPageProp<{ sound_settings: SoundSettings }>('current_user', {
30-
sound_settings: { type: 'standard', level: 5 },
30+
const defaultSoundSettings: SoundSettings = { type: 'standard', level: 5 };
31+
const initialSoundSettings = getPageProp<{ sound_settings: SoundSettings }>('current_user', {
32+
sound_settings: defaultSoundSettings,
3133
}).sound_settings;
32-
const soundType = soundSettings.type;
33-
const defaultSoundLevel = soundSettings.level * 0.1;
34-
const tournamentSoundLevel = isUndefined(soundSettings.tournament_level)
35-
? defaultSoundLevel
36-
: soundSettings.tournament_level * 0.1;
3734

38-
const audio = (type: string = soundType, volume: number = defaultSoundLevel) =>
35+
let currentSoundSettings = { ...defaultSoundSettings, ...initialSoundSettings };
36+
37+
const getSoundType = () => currentSoundSettings.type;
38+
const getDefaultSoundLevel = () => currentSoundSettings.level * 0.1;
39+
const getTournamentSoundLevel = () => {
40+
const tournamentLevel =
41+
currentSoundSettings.tournamentLevel ?? currentSoundSettings.tournament_level;
42+
43+
return isUndefined(tournamentLevel) ? getDefaultSoundLevel() : tournamentLevel * 0.1;
44+
};
45+
46+
const configureSound = (settings: SoundSettings) => {
47+
currentSoundSettings = { ...defaultSoundSettings, ...settings };
48+
};
49+
50+
const audio = (type: string = getSoundType(), volume: number = getDefaultSoundLevel()) =>
3951
new Howl({
4052
src: audioPaths[type as keyof typeof audioPaths],
4153
sprite: (audioConfigs[type as keyof typeof audioConfigs] as { sprite?: unknown })
@@ -48,7 +60,7 @@ const getAssetPlayer = (path: string) => {
4860
if (!assetPlayers[path]) {
4961
assetPlayers[path] = new Howl({
5062
src: path,
51-
volume: defaultSoundLevel,
63+
volume: getDefaultSoundLevel(),
5264
});
5365
}
5466

@@ -58,27 +70,27 @@ const getAssetPlayer = (path: string) => {
5870
const sound = {
5971
play: (type: string, soundLevel?: number) => {
6072
const isMute = JSON.parse((localStorage.getItem('ui_mute_sound') || false) as string);
73+
if (getSoundType() === 'silent' || isMute) return;
6174
const soundEffect = audio();
62-
if (soundType === 'silent' || isMute) return;
63-
Howler.volume(isUndefined(soundLevel) ? defaultSoundLevel : soundLevel);
75+
Howler.volume(isUndefined(soundLevel) ? getDefaultSoundLevel() : soundLevel);
6476
soundEffect.play(type);
6577
},
6678
playAsset: (path: string, soundLevel?: number) => {
6779
const isMute = JSON.parse((localStorage.getItem('ui_mute_sound') || false) as string);
68-
if (soundType === 'silent' || isMute) return;
69-
Howler.volume(isUndefined(soundLevel) ? defaultSoundLevel : soundLevel);
80+
if (getSoundType() === 'silent' || isMute) return;
81+
Howler.volume(isUndefined(soundLevel) ? getDefaultSoundLevel() : soundLevel);
7082
getAssetPlayer(path).play();
7183
},
7284
playTournamentAsset: (path: string) => {
7385
const isMute = JSON.parse((localStorage.getItem('ui_mute_sound') || false) as string);
74-
if (soundType === 'silent' || isMute) return;
75-
Howler.volume(tournamentSoundLevel);
86+
if (getSoundType() === 'silent' || isMute) return;
87+
Howler.volume(getTournamentSoundLevel());
7688
const player = getAssetPlayer(path);
7789
player.volume(1);
7890
player.play();
7991
},
8092
stop: () => Howler.stop(),
81-
toggle: (volume: number = defaultSoundLevel) => {
93+
toggle: (volume: number = getDefaultSoundLevel()) => {
8294
Howler.volume(volume);
8395
},
8496
};
@@ -98,5 +110,5 @@ const createPlayer = () => ({
98110
stop: () => Howler.stop(),
99111
});
100112

101-
export { createPlayer };
113+
export { configureSound, createPlayer };
102114
export default sound;

apps/codebattle/assets/js/widgets/pages/RoomWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function RoomWidget({ pageName, mainMachine, taskMachine, editorMachine }: RoomW
9090
invisible: !visible,
9191
})}
9292
>
93-
<div className="row no-gutter cb-game px-1">
93+
<div className="row no-gutters cb-game px-1">
9494
{showBattleRoom && (
9595
<>
9696
<InfoWidget viewMode={viewMode} />

apps/codebattle/assets/js/widgets/pages/game/ChatWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function ChatWidget() {
7070
inputRef={inputRef as React.RefObject<HTMLInputElement>}
7171
request={menuRequest}
7272
>
73-
<div className="d-flex cb-bg-panel shadow-sm h-100 cb-rounded">
73+
<div className="d-flex cb-game-chat-layout cb-bg-panel shadow-sm h-100 cb-rounded">
7474
<div
7575
className={cn(
7676
'd-flex flex-column flex-grow-1 position-relative p-0 h-100 mh-100 rounded-left',

0 commit comments

Comments
 (0)