Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4dae9cc
Migrate React frontend off react-bootstrap onto Mantine (Phase 0/1)
ReDBrother Aug 7, 2026
cbc3524
Convert shared leaf components to Mantine primitives (Phase 2)
ReDBrother Aug 7, 2026
7bac69b
Convert more shared leaf components to Mantine (Phase 2)
ReDBrother Aug 7, 2026
eebcaf1
improves
ReDBrother Aug 7, 2026
7ac7caa
Map cb border tokens into Mantine theme (Phase 2)
ReDBrother Aug 7, 2026
a4f8f9d
Convert loading components to Mantine Loader (Phase 2)
ReDBrother Aug 7, 2026
3eec871
Convert small text/layout leaf components to Mantine (Phase 2)
ReDBrother Aug 7, 2026
c1f6c62
Convert TournamentDescription layout to Mantine (Phase 2)
ReDBrother Aug 7, 2026
a9bf105
Convert ChatHeader to Mantine (Phase 2)
ReDBrother Aug 7, 2026
f7c7de2
fixes
ReDBrother Aug 7, 2026
794029b
Convert chat Message + Messages to Mantine (Phase 2)
ReDBrother Aug 7, 2026
388a2d2
Update Mantine migration spec: Phase 2 progress + gotchas
ReDBrother Aug 7, 2026
076f78b
Convert User* cluster to Mantine (Phase 2)
ReDBrother Aug 7, 2026
c09371a
Convert SideScrollControls to Mantine (Phase 2)
ReDBrother Aug 7, 2026
c7eebf9
Convert ScheduleNavigationBar to Mantine (Phase 2)
ReDBrother Aug 7, 2026
35ec615
Convert Rooms to Mantine + port cb-btn-secondary hover to theme (Phas…
ReDBrother Aug 7, 2026
b71cb76
Convert Invites cluster to Mantine, drop dead DropdownMenuDefault (Ph…
ReDBrother Aug 7, 2026
0414a11
Convert AccordeonBox to Mantine, drop dead Menu (Phase 2)
ReDBrother Aug 7, 2026
858d9f5
Convert FeedbackWidget to Mantine (Phase 2)
ReDBrother Aug 7, 2026
3687ea2
Convert FeedbackAlertNotification to Mantine (Phase 2)
ReDBrother Aug 7, 2026
2da5fa2
Record remaining-leaf audit in Mantine migration spec (Phase 2)
ReDBrother Aug 7, 2026
5cc2fe4
Convert ChatInput to Mantine (Phase 2)
ReDBrother Aug 7, 2026
4a77292
Convert SeasonLeaderboard to Mantine (Phase 2)
ReDBrother Aug 7, 2026
6f8eca8
Convert PlayerInsightsModal to Mantine (Phase 2)
ReDBrother Aug 7, 2026
eb50f45
Strip Bootstrap classes from EmojiTooltip and ChatContextMenu (Phase 2)
ReDBrother Aug 7, 2026
4345486
Convert settings page to Mantine (Phase 2)
ReDBrother Aug 7, 2026
c896752
Convert profile leaf components to Mantine (Phase 2)
ReDBrother Aug 7, 2026
386f8af
Convert UserProfile container to Mantine (Phase 2)
ReDBrother Aug 7, 2026
1ac369d
Convert lobby tournament lists to Mantine (Phase 2)
ReDBrother Aug 7, 2026
0cf1823
Convert lobby ShowButton to Mantine (Phase 2)
ReDBrother Aug 7, 2026
892220d
Convert lobby GameActionButton/GameCard/TournamentCard to Mantine (Ph…
ReDBrother Aug 7, 2026
ab8cf3b
Note Players/ActiveGames table-cell coupling in Mantine spec (Phase 2)
ReDBrother Aug 7, 2026
387af61
Convert lobby games tables to Mantine (Phase 2)
ReDBrother Aug 7, 2026
22196fb
Convert lobby Leaderboard to Mantine (Phase 2)
ReDBrother Aug 7, 2026
7a9eca3
Convert lobby GameRoomPreview + LobbyLoading to Mantine (Phase 2)
ReDBrother Aug 7, 2026
e2643e8
Convert lobby ChatActionModal to Mantine (Phase 2)
ReDBrother Aug 7, 2026
bb9e449
Convert lobby TournamentListItem to Mantine (Phase 2)
ReDBrother Aug 7, 2026
3a5d8bb
Convert lobby SeasonProfilePanel to Mantine (Phase 2)
ReDBrother Aug 7, 2026
9b67a4f
Convert lobby CodebattleLeagueDescription to Mantine (Phase 2)
ReDBrother Aug 7, 2026
87012ea
Log lobby ChatActionModal/TournamentListItem/SeasonProfilePanel/Leagu…
ReDBrother Aug 7, 2026
c63d0c0
Convert lobby TournamentModal to Mantine (Phase 2)
ReDBrother Aug 7, 2026
ffe05ba
Convert lobby LobbyChat to Mantine (Phase 2)
ReDBrother Aug 7, 2026
9170381
Convert lobby CreateGameDialog layout to Mantine (Phase 2)
ReDBrother Aug 7, 2026
292dc64
Convert lobby LobbyWidget container to Mantine (Phase 2)
ReDBrother Aug 7, 2026
ffaffc3
Log lobby TournamentModal/LobbyChat/CreateGameDialog/LobbyWidget conv…
ReDBrother Aug 7, 2026
edf2f92
Wrap LobbyLoading shell test in MantineTestProvider (Phase 2)
ReDBrother Aug 7, 2026
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
634 changes: 634 additions & 0 deletions apps/codebattle/assets/MANTINE_MIGRATION.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion apps/codebattle/assets/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ $cb-rookie-bg: $iron;
}

@import 'bootstrap/scss/bootstrap';
// Mantine core styles are wrapped in `@layer mantine`. Unlayered Bootstrap above
// always wins on conflicts, so existing Bootstrap-styled UI is unaffected while
// Mantine components still get their own styling.
@import '@mantine/core/styles.layer.css';
@import 'nprogress/nprogress.css';
@import 'gamePreview';
@import 'skeleton';
Expand Down Expand Up @@ -4635,7 +4639,7 @@ a.cb-text-light:hover {
}

.cb-settings-social-links {
> .d-flex {
> * {
margin-bottom: 8px !important;
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
Expand Down
64 changes: 36 additions & 28 deletions apps/codebattle/assets/js/__tests__/ChatMessageDelete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,26 @@ import React from 'react';

import Message from '../widgets/components/Message';

import { MantineTestProvider } from './helpers/mantine';

vi.mock('react-redux', () => ({ useSelector: () => ({ name: 'General' }) }));

test('shows a delete action on the current user own message and calls back with its id', async () => {
const user = userEvent.setup();
const handleDelete = vi.fn();

render(
<Message
id={7}
name="Alice"
userId={42}
currentUserId={42}
time={1}
text="my message"
onDeleteMessage={handleDelete}
/>,
<MantineTestProvider>
<Message
id={7}
name="Alice"
userId={42}
currentUserId={42}
time={1}
text="my message"
onDeleteMessage={handleDelete}
/>
</MantineTestProvider>,
);

await user.click(screen.getByRole('button', { name: 'Delete message' }));
Expand All @@ -29,15 +33,17 @@ test('shows a delete action on the current user own message and calls back with

test('hides the delete action on another user message for a non-privileged user', () => {
render(
<Message
id={7}
name="Bob"
userId={99}
currentUserId={42}
time={1}
text="not mine"
onDeleteMessage={vi.fn()}
/>,
<MantineTestProvider>
<Message
id={7}
name="Bob"
userId={99}
currentUserId={42}
time={1}
text="not mine"
onDeleteMessage={vi.fn()}
/>
</MantineTestProvider>,
);

expect(screen.queryByRole('button', { name: 'Delete message' })).not.toBeInTheDocument();
Expand All @@ -48,16 +54,18 @@ test('shows the delete action on another user message when the viewer can delete
const handleDelete = vi.fn();

render(
<Message
id={7}
name="Bob"
userId={99}
currentUserId={42}
time={1}
text="not mine"
canDeleteAny
onDeleteMessage={handleDelete}
/>,
<MantineTestProvider>
<Message
id={7}
name="Bob"
userId={99}
currentUserId={42}
time={1}
text="not mine"
canDeleteAny
onDeleteMessage={handleDelete}
/>
</MantineTestProvider>,
);

await user.click(screen.getByRole('button', { name: 'Delete message' }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import CreateGameDialog from '../widgets/pages/lobby/CreateGameDialog';
import reducers from '../widgets/slices';

import { getTestData } from './helpers';
import { MantineTestProvider } from './helpers/mantine';

vi.mock('@/inertia/pageProps', () => {
const pageProps = {
Expand Down Expand Up @@ -125,7 +126,9 @@ beforeAll(() => {

vdom = (
<Provider store={store}>
<CreateGameDialog hideModal={noop} />
<MantineTestProvider>
<CreateGameDialog hideModal={noop} />
</MantineTestProvider>
</Provider>
);
});
Expand Down
26 changes: 15 additions & 11 deletions apps/codebattle/assets/js/__tests__/GameActionButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import { render, screen } from '@testing-library/react';

import GameActionButton from '../widgets/pages/lobby/GameActionButton';

import { MantineTestProvider } from './helpers/mantine';

test('uses the full-width localized continue action outside the games table', () => {
render(
<GameActionButton
type="card"
currentUserId={7}
game={{
id: 42,
state: 'playing',
level: 'easy',
players: [{ id: 7 }],
}}
/>,
<MantineTestProvider>
<GameActionButton
type="card"
currentUserId={7}
game={{
id: 42,
state: 'playing',
level: 'easy',
players: [{ id: 7 }],
}}
/>
</MantineTestProvider>,
);

expect(screen.getByRole('link', { name: 'Continue' })).toHaveClass('w-100');
expect(screen.getByRole('link', { name: 'Continue' })).toHaveAttribute('data-block', 'true');
});
24 changes: 17 additions & 7 deletions apps/codebattle/assets/js/__tests__/Localization.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import TournamentDescription from '../widgets/components/TournamentDescription';
import TournamentPreviewPanel from '../widgets/components/TournamentPreviewPanel';
import { localizeTournamentName } from '../widgets/utils/localizeTournamentName';

import { MantineTestProvider } from './helpers/mantine';

const translations = [
['Points', 'Очки'],
['Today', 'Сегодня'],
Expand Down Expand Up @@ -66,7 +68,9 @@ test('localizes the empty invites state', () => {

render(
<Provider store={store}>
<InvitesList list={[]} currentUserId={1} />
<MantineTestProvider>
<InvitesList list={[]} currentUserId={1} />
</MantineTestProvider>
</Provider>,
);

Expand All @@ -75,11 +79,13 @@ test('localizes the empty invites state', () => {

test('localizes tournament preview dates and point labels', () => {
render(
<TournamentPreviewPanel
tournament={{ grade: 'rookie' }}
start="2026-08-05T02:00:00"
end="2026-08-05T02:15:00"
/>,
<MantineTestProvider>
<TournamentPreviewPanel
tournament={{ grade: 'rookie' }}
start="2026-08-05T02:00:00"
end="2026-08-05T02:15:00"
/>
</MantineTestProvider>,
);

expect(screen.getByText('Дата начала: 5 августа 2026')).toBeInTheDocument();
Expand All @@ -88,7 +94,11 @@ test('localizes tournament preview dates and point labels', () => {
});

test('localizes tournament highlights and grade names', () => {
render(<TournamentDescription tournament={{ grade: 'rookie' }} />);
render(
<MantineTestProvider>
<TournamentDescription tournament={{ grade: 'rookie' }} />
</MantineTestProvider>,
);

expect(screen.getByText('Главное о турнире:')).toBeInTheDocument();
expect(screen.getByText('Задачи: 4 уникальных алгоритмических задач')).toBeInTheDocument();
Expand Down
38 changes: 18 additions & 20 deletions apps/codebattle/assets/js/__tests__/PopoverStickOnHover.test.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
import React from 'react';

import { act, fireEvent, render, screen } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';

import PopoverStickOnHover from '../widgets/components/PopoverStickOnHover';
import { theme } from '../widgets/ui/theme';

describe('PopoverStickOnHover', () => {
afterEach(() => {
vi.useRealTimers();
});

test('keeps the delayed hover timer across parent rerenders', () => {
vi.useFakeTimers();
const renderWithMantine = (ui: React.ReactNode) =>
render(
<MantineProvider theme={theme} forceColorScheme="dark">
{ui}
</MantineProvider>,
);

describe('PopoverStickOnHover', () => {
test('reveals the popover content when hovering the target', async () => {
const user = userEvent.setup();
const component = <span>user details</span>;
const view = render(
<PopoverStickOnHover id="user-info" delay={400} component={component}>
<button type="button">Ada</button>
</PopoverStickOnHover>,
);

fireEvent.mouseEnter(screen.getByRole('button', { name: 'Ada' }));

view.rerender(
<PopoverStickOnHover id="user-info" delay={400} component={component}>
renderWithMantine(
<PopoverStickOnHover id="user-info" delay={0} component={component}>
<button type="button">Ada</button>
</PopoverStickOnHover>,
);

act(() => vi.advanceTimersByTime(399));
expect(screen.queryByText('user details')).not.toBeInTheDocument();

act(() => vi.advanceTimersByTime(1));
expect(screen.getByText('user details')).toBeInTheDocument();
await user.hover(screen.getByRole('button', { name: 'Ada' }));

expect(await screen.findByText('user details')).toBeInTheDocument();
});
});
Loading
Loading