From 4dae9cc5b6097d4544233578fd2700073edf264c Mon Sep 17 00:00:00 2001 From: ReDBrother Date: Fri, 7 Aug 2026 12:49:04 +0300 Subject: [PATCH 01/46] Migrate React frontend off react-bootstrap onto Mantine (Phase 0/1) Infra (Phase 0): add @mantine/core + @mantine/hooks v9, PostCSS setup, CSS-layer coexistence with Bootstrap, dark theme, MantineProvider on all React roots. Spacing scale matched to Bootstrap-4 spacers. Components (Phase 1): replace all react-bootstrap components with Mantine equivalents behind compatible wrappers (CbModal, PopoverStickOnHover); remove the react-bootstrap dependency. Global Bootstrap CSS stays for .heex templates (Phase 3). Co-Authored-By: Claude --- apps/codebattle/assets/MANTINE_MIGRATION.md | 162 ++++++++ apps/codebattle/assets/css/style.scss | 4 + .../js/__tests__/PopoverStickOnHover.test.tsx | 38 +- .../js/__tests__/RootContainer.test.tsx | 4 +- .../TournamentMainControlButtons.test.tsx | 10 +- .../assets/js/__tests__/UserSettings.test.tsx | 47 +-- .../assets/js/__tests__/helpers/mantine.tsx | 13 + apps/codebattle/assets/js/inertia.tsx | 3 +- .../js/widgets/components/AccordeonBox.tsx | 17 +- .../js/widgets/components/BootstrapModal.tsx | 25 -- .../assets/js/widgets/components/CbModal.tsx | 122 ++++++ .../components/FeedbackAlertNotification.tsx | 10 +- .../js/widgets/components/FeedbackWidget.tsx | 20 +- .../widgets/components/InvitesContainer.tsx | 106 +++--- .../components/OverlayTriggerCompat.tsx | 230 ------------ .../components/PlayerInsightsModal/index.tsx | 10 +- .../components/PopoverStickOnHover.tsx | 100 ++--- .../js/widgets/components/ResultIcon.tsx | 11 +- .../assets/js/widgets/components/Rooms.tsx | 45 +-- .../assets/js/widgets/components/UserInfo.tsx | 8 +- apps/codebattle/assets/js/widgets/index.tsx | 52 ++- .../js/widgets/pages/admin/AdminWidget.tsx | 4 +- .../event/EventStageConfirmationModal.tsx | 4 +- .../js/widgets/pages/game/AnimationModal.tsx | 8 +- .../pages/game/CodebattleSliderBar.tsx | 18 +- .../widgets/pages/game/GameActionButtons.tsx | 96 ++--- .../js/widgets/pages/game/GameResult.tsx | 11 +- .../js/widgets/pages/game/GameResultIcon.tsx | 19 +- .../game/NextStageGroupTournamentModal.tsx | 6 +- .../pages/game/PremiumRestrictionModal.tsx | 14 +- .../pages/game/TaskDescriptionModal.tsx | 14 +- .../pages/game/TaskLanguageSelection.tsx | 35 +- .../pages/game/TournamentAwardModal.tsx | 6 +- .../pages/game/TournamentStatisticsModal.tsx | 4 +- .../js/widgets/pages/game/VimModeButton.tsx | 1 - .../pages/headToHeadPage/HeadToHeadPage.tsx | 5 +- .../js/widgets/pages/lobby/Announcement.tsx | 3 - .../widgets/pages/lobby/ChatActionModal.tsx | 4 +- .../js/widgets/pages/lobby/Leaderboard.tsx | 4 +- .../js/widgets/pages/lobby/LobbyWidget.tsx | 4 +- .../widgets/pages/lobby/TournamentModal.tsx | 8 +- .../js/widgets/pages/schedule/EventModal.tsx | 8 +- .../widgets/pages/settings/UserSettings.tsx | 15 +- .../pages/settings/UserSettingsForm.tsx | 83 +++-- .../widgets/pages/tournament/DetailsModal.tsx | 24 +- .../pages/tournament/EditTournament.tsx | 13 +- .../tournament/MatchConfirmationModal.tsx | 30 +- .../StartRoundConfirmationModal.tsx | 21 +- .../tournament/TournamentDescriptionModal.tsx | 21 +- .../TournamentMainControlButtons.tsx | 51 ++- .../pages/tournament/UsersMatchList.tsx | 32 +- apps/codebattle/assets/js/widgets/ui/alert.ts | 13 + apps/codebattle/assets/js/widgets/ui/theme.ts | 63 ++++ .../assets/js/widgets/ui/withMantine.tsx | 14 + apps/codebattle/package.json | 5 +- apps/codebattle/pnpm-lock.yaml | 351 ++++++++++++++---- apps/codebattle/postcss.config.js | 14 +- apps/codebattle/vitest.setup.ts | 28 ++ 58 files changed, 1174 insertions(+), 917 deletions(-) create mode 100644 apps/codebattle/assets/MANTINE_MIGRATION.md create mode 100644 apps/codebattle/assets/js/__tests__/helpers/mantine.tsx delete mode 100644 apps/codebattle/assets/js/widgets/components/BootstrapModal.tsx create mode 100644 apps/codebattle/assets/js/widgets/components/CbModal.tsx delete mode 100644 apps/codebattle/assets/js/widgets/components/OverlayTriggerCompat.tsx create mode 100644 apps/codebattle/assets/js/widgets/ui/alert.ts create mode 100644 apps/codebattle/assets/js/widgets/ui/theme.ts create mode 100644 apps/codebattle/assets/js/widgets/ui/withMantine.tsx diff --git a/apps/codebattle/assets/MANTINE_MIGRATION.md b/apps/codebattle/assets/MANTINE_MIGRATION.md new file mode 100644 index 000000000..ede20d952 --- /dev/null +++ b/apps/codebattle/assets/MANTINE_MIGRATION.md @@ -0,0 +1,162 @@ +# Bootstrap → Mantine migration + +Living spec for moving the React frontend off Bootstrap/react-bootstrap onto +Mantine. Phases 0–1 are **done**; Phases 2–3 are **planned** and described here +in enough detail to resume without re-discovery. + +## Scope & standing decisions + +- **React only.** Global Bootstrap CSS (`bootstrap@4.6.2`, imported in + `assets/css/style.scss` and `assets/js/app.ts`) **stays** — 53 `.heex` server + templates still use Bootstrap utility classes. Do **not** remove the + `bootstrap` dependency or its CSS until the heex templates are migrated + (Phase 3, out of current scope). +- **Idiomatic Mantine.** Prefer Mantine layout primitives (`Group`/`Stack`/ + `Flex`/`Box`) + style props (`mb`, `p`, `c`) over porting utility classes 1:1. +- **Incremental, per-widget.** One widget/page per PR, leaf → container, verify + each in the browser before merging. +- **Mantine version:** `@mantine/core` + `@mantine/hooks` v9. + +## Current status + +| Phase | What | Status | +|------|------|--------| +| 0 | Infra: Mantine deps, PostCSS, CSS-layer coexistence, theme, `MantineProvider` on all roots | ✅ done | +| 1 | Replace `react-bootstrap` **components** with Mantine; remove `react-bootstrap` dep | ✅ done | +| 2 | Convert Bootstrap **utility classes** in the ~244 React files to idiomatic Mantine | ⬜ planned | +| 3 | Migrate `.heex` templates; fully remove Bootstrap CSS + `bootstrap` dep | ⬜ planned (out of current scope) | + +## Coexistence model (how Bootstrap + Mantine live together) + +- Mantine core styles are imported as **`@mantine/core/styles.layer.css`** (in + `style.scss`), which wraps everything in `@layer mantine`. Unlayered Bootstrap + always wins on conflicts, so existing Bootstrap-styled UI is unaffected. +- Because Bootstrap wins, Mantine components that also carry Bootstrap/custom + classes (e.g. Alerts keeping `.alert-dark-theme`, Tables keeping `.table`) get + their look from those classes. Phase 2 removes the classes and lets Mantine + style them. + +## Key files (Phase 0/1 infra) + +- `assets/js/widgets/ui/theme.ts` — Mantine theme. Custom colors `cbSecondary` + (#3a3f50), `cbSuccess` (#46a077), `brand` (orange #ee3737). **Spacing scale is + matched to Bootstrap-4 spacers**: `xs=.25rem, sm=.5rem, md=1rem, lg=1.5rem, + xl=3rem` — so `mb-2 → mb="sm"`, `p-3 → p="md"`, etc. keep the same rhythm. +- `assets/js/widgets/ui/withMantine.tsx` — provider wrapper; applied to every + React root in `widgets/index.tsx` and the Inertia root in `inertia.tsx`. + `forceColorScheme="dark"` (single baked-in dark look, no runtime toggle). +- `assets/js/widgets/ui/alert.ts` — `bootstrapAlertColor(variant)` maps BS alert + variants → Mantine colors. +- `assets/js/widgets/components/CbModal.tsx` — app modal wrapper: keeps a compound + API (`Modal` + `Modal.Header/Title/Body/Footer`) but renders a Mantine `Modal` + underneath, so ~21 modal call sites are untouched. Default `size="lg"` + (Bootstrap's `.modal-dialog { min-width: 700px }` no longer applies to Mantine's + DOM). +- `assets/js/widgets/components/PopoverStickOnHover.tsx` — Mantine `HoverCard` + behind the original API; exports `Placement` (= `FloatingPosition`). + +## Gotchas / learnings (read before Phase 2) + +1. **Mantine v9 components throw without `MantineProvider`.** Any test that + mounts a Mantine component must wrap in `MantineTestProvider` + (`assets/js/__tests__/helpers/mantine.tsx`). The app itself is fine — all + roots use `withMantine`. +2. **jsdom polyfills** for Mantine are in `vitest.setup.ts`: `window.matchMedia` + and `ResizeObserver`. Keep them. +3. **Portals in tests.** Mantine `Modal`/`Menu`/`HoverCard`/`Popover` render in a + portal. Query with `screen.*` and use async `findBy*` for open-on-interaction + content (modals/menus have mount transitions — sync `getByRole` races). +4. **`@/ui/*` path alias is broken in Vite's resolver** (only specific `@/` + subpaths are aliased; `ui` isn't). Import theme/helpers via **relative paths** + (`../ui/...`), not `@/ui/...`. tsc accepts `@/*` but Vite/vitest will fail to + resolve. +5. **Custom-event button theming.** `cb-custom-event-btn-*` classes (in + `external.scss`/`custom.scss`) are event-branded color overrides used across + ~8 files, applied conditionally via `className` on Mantine `Button`s. They are + pure color overrides and still win over Mantine's layer — handle them as a set + in Phase 2, don't drop them piecemeal. +6. **Menu items only render when open.** react-bootstrap `Dropdown.Menu` + rendered items always; Mantine `Menu.Dropdown` mounts on open. Use + `keepMounted` if a test/behavior depends on items being in the DOM while + closed (see `UserSettingsForm` language menu). + +--- + +## Phase 2 — Utility classes → idiomatic Mantine (the big one) + +**Goal:** remove Bootstrap utility classes from React components, replacing them +with Mantine primitives + style props. ~244 files; work per-widget. + +### Conversion vocabulary + +| Bootstrap | Mantine | +|-----------|---------| +| `d-flex` + `align-items-*`/`justify-content-*` | `` (row) / `` with `align`/`justify` | +| `d-flex flex-column` | `` (or ``) | +| `row` / `col-*` / `col-md-4` | `` + `` or `` | +| spacing `mb-2 p-3 mr-2 px-1 py-2 …` | style props `mb="sm" p="md" mr="sm"` (scale already matched) | +| `text-center` / `text-white` / `text-muted` | ``, `c="dimmed"` | +| `font-weight-bold` | `fw={700}` | +| `card` / `card-body` / `card-header` | `` / `` or `` | +| `badge badge-*` | `` | +| `border` / `rounded` / `rounded-lg` / `shadow` | `withBorder`, `radius="md"`, `shadow="sm"` | +| `w-100` / `h-100` | `w="100%"` / `h="100%"` | +| `d-none` (responsive show/hide) | `visibleFrom`/`hiddenFrom` or `display` style prop | +| `btn btn-secondary cb-btn-secondary` (native buttons left from Phase 1) | ` - , - ); - fireEvent.mouseEnter(screen.getByRole('button', { name: 'Ada' })); - - view.rerender( - + renderWithMantine( + , ); - 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(); }); }); diff --git a/apps/codebattle/assets/js/__tests__/RootContainer.test.tsx b/apps/codebattle/assets/js/__tests__/RootContainer.test.tsx index 237d72213..f5a0a1cc5 100644 --- a/apps/codebattle/assets/js/__tests__/RootContainer.test.tsx +++ b/apps/codebattle/assets/js/__tests__/RootContainer.test.tsx @@ -16,6 +16,8 @@ import task, { config as taskConfig } from '../widgets/machines/task'; import RootContainer from '../widgets/pages/RoomWidget'; import reducers from '../widgets/slices'; +import { MantineTestProvider } from './helpers/mantine'; + vi.mock('pixelmatch', () => ({ default: () => {} })); vi.mock('monaco-editor', () => ({ @@ -231,7 +233,7 @@ editor.initial = 'idle'; const setup = (jsx: ReactElement) => ({ user: userEvent.setup(), - ...render(jsx), + ...render({jsx}), }); test('rendering preview game component', async () => { diff --git a/apps/codebattle/assets/js/__tests__/TournamentMainControlButtons.test.tsx b/apps/codebattle/assets/js/__tests__/TournamentMainControlButtons.test.tsx index f7c903af6..1582ef815 100644 --- a/apps/codebattle/assets/js/__tests__/TournamentMainControlButtons.test.tsx +++ b/apps/codebattle/assets/js/__tests__/TournamentMainControlButtons.test.tsx @@ -6,6 +6,8 @@ import { Provider } from 'react-redux'; import TournamentMainControlButtons from '../widgets/pages/tournament/TournamentMainControlButtons'; +import { MantineTestProvider } from './helpers/mantine'; + vi.mock('@fortawesome/react-fontawesome', () => ({ FontAwesomeIcon: 'img', })); @@ -45,9 +47,11 @@ function renderComponent(props = {}) { }; return render( - - - , + + + + + , ); } diff --git a/apps/codebattle/assets/js/__tests__/UserSettings.test.tsx b/apps/codebattle/assets/js/__tests__/UserSettings.test.tsx index 763d88a7f..399242710 100644 --- a/apps/codebattle/assets/js/__tests__/UserSettings.test.tsx +++ b/apps/codebattle/assets/js/__tests__/UserSettings.test.tsx @@ -1,13 +1,15 @@ import '@testing-library/jest-dom'; import { configureStore, combineReducers } from '@reduxjs/toolkit'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React, { type ReactElement, type ReactNode } from 'react'; +import React, { type ReactElement } from 'react'; import { Provider } from 'react-redux'; import UserSettings from '../widgets/pages/settings'; import reducers from '../widgets/slices'; +import { MantineTestProvider } from './helpers/mantine'; + vi.mock('@fortawesome/react-fontawesome', () => ({ FontAwesomeIcon: () =>