diff --git a/.gitignore b/.gitignore index b7a1db919..41ac08d55 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ priv/plts/*.plt.hash *.pnpm-store .customization/ +{{pkgetc}}/ diff --git a/apps/codebattle/assets/MANTINE_MIGRATION.md b/apps/codebattle/assets/MANTINE_MIGRATION.md new file mode 100644 index 000000000..c6cc7f9cc --- /dev/null +++ b/apps/codebattle/assets/MANTINE_MIGRATION.md @@ -0,0 +1,1210 @@ +# Bootstrap → Mantine migration + +Living spec for moving the React frontend off Bootstrap/react-bootstrap onto +Mantine. Phases 0–1 are **done**; Phase 2 is **in progress** (shared leaf +components converted — see the progress log under Phase 2); Phase 3 is +**planned**. 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 — **verified 2026-08-11**: no `react-bootstrap` import anywhere in source (single hit is a comment in `PopoverStickOnHover.tsx`), no dep in `apps/codebattle/package.json`, `@mantine/core`/`@mantine/hooks` at `^9.5.1`. `bootstrap@4.6.2` correctly retained (heex, Phase 3) | +| 2 | Convert Bootstrap **utility classes** in the ~244 React files to idiomatic Mantine | ✅ done — all React pages converted; the react-select swap, the `RoomWidget` grid slice, the passthrough/typography leftovers, the `TaskAssignment` task-zoom port, and the **design-class theme port** (shared `cb-bg-panel`/`cb-bg-highlight-panel`/`cb-border-color`/`cb-rounded`/`cb-text`/`cb-text-light` now have `theme.ts` homes; `widgets/components/**` + all `CbModal` `contentClassName="cb-text"` callers + **all page-level usages** converted, only heex/react-contexify use the SCSS classes until Phase 3) are done (2026-08-12); remaining: `SoundToggle` (really Phase 3) | +| 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. + Also exports **`cssVariablesResolver`** (Phase 2) that sets + `--mantine-color-default-border` = `#4c4c5a` (`$cb-border-color`) in the dark + scheme, so `` matches the legacy `.cb-border-color` + panels. It is passed to `MantineProvider` in **both** `withMantine.tsx` and the + test provider `helpers/mantine.tsx`. `cb-rounded` (`$cb-border-radius` 0.5rem) + → Mantine `radius="md"` (md = 0.5rem). +- `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). +7. **Unlayered Bootstrap beats Mantine — watch `border-radius` on buttons.** + Bootstrap is imported unlayered (`style.scss` `@import 'bootstrap/scss/bootstrap'`) + while Mantine is in `@layer mantine`, so **any** unlayered rule wins over + Mantine regardless of specificity. Concretely, `style.scss` has + `.btn { border-radius: unset }` and utilities like `rounded-top`. A Mantine + ` - + diff --git a/apps/codebattle/lib/codebattle_web/live/event/leaderboard_view.ex b/apps/codebattle/lib/codebattle_web/live/event/leaderboard_view.ex index fc625b0a9..0f765d2d8 100644 --- a/apps/codebattle/lib/codebattle_web/live/event/leaderboard_view.ex +++ b/apps/codebattle/lib/codebattle_web/live/event/leaderboard_view.ex @@ -14,30 +14,30 @@ defmodule CodebattleWeb.Live.Event.LeaderboardView do @impl true def render(assigns) do ~H""" -
- - +
+
+ - - - - + + + + <%= for item <- @leaderboard_list do %> - - + - - - diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/connections.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/connections.html.heex index ca1dcbaed..66ff18556 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/connections.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/connections.html.heex @@ -1,3 +1,3 @@ -
+
diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/_form.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/_form.html.heex index 3cacf4d00..ac096fd7b 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/_form.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/_form.html.heex @@ -1,52 +1,52 @@ -<%= form_for(@changeset, @action, [class: "col-8 offset-2"], fn f -> %> -
+<%= form_for(@changeset, @action, [class: "cb-col-8 cb-offset-2"], fn f -> %> +
- Slug - Unique identifier for API token batches and admin lookup + Slug + Unique identifier for API token batches and admin lookup
{text_input(f, :slug, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", maxlength: "255", required: true )} {error_tag(f, :slug)}
-
+
- Time to solve (sec) + Time to solve (sec)
{number_input(f, :time_to_solve_sec, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1", required: true )} {error_tag(f, :time_to_solve_sec)}
-
+
- Runner URL - + Runner URL + HTTP endpoint that will receive selected latest player solutions
{text_input(f, :runner_url, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", maxlength: "500", required: false )} {error_tag(f, :runner_url)}
-
+
{submit("Save", phx_disable_with: "Saving...", - class: "btn btn-success text-white mb-2 cb-rounded" + class: "cb-btn cb-btn-success cb-text-white cb-mb-2 cb-rounded" )} {link("Back", to: Routes.group_task_path(@conn, :index), - class: "btn btn-link cb-text ml-auto" + class: "cb-btn cb-btn-link cb-text cb-ml-auto" )}
<% end) %> diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit.html.heex index 37ca6f43d..197f995d3 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit.html.heex @@ -1,5 +1,5 @@ -
-

Edit Group Task

+
+

Edit Group Task

{render( "_form.html", assigns |> Map.put(:action, Routes.group_task_path(@conn, :update, @group_task)) diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit_solution.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit_solution.html.heex index 2f95f13d7..3f97d9d58 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit_solution.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/edit_solution.html.heex @@ -1,39 +1,39 @@ -
-

Edit Group Task Solution

+
+

Edit Group Task Solution

-
+

Group Task: {@group_task.slug}

User ID: {@solution.user_id}

- <%= form_for(@changeset, Routes.update_group_task_solution_path(@conn, :update_solution, @group_task, @solution.id), [class: "col-8 offset-2"], fn f -> %> -
- Lang + <%= form_for(@changeset, Routes.update_group_task_solution_path(@conn, :update_solution, @group_task, @solution.id), [class: "cb-col-8 cb-offset-2"], fn f -> %> +
+ Lang {text_input(f, :lang, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", required: true )} {error_tag(f, :lang)}
-
- Solution +
+ Solution {textarea(f, :solution, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", rows: 18, required: true )} {error_tag(f, :solution)}
-
+
{submit("Save", phx_disable_with: "Saving...", - class: "btn btn-success text-white mb-2 cb-rounded" + class: "cb-btn cb-btn-success cb-text-white cb-mb-2 cb-rounded" )} {link("Back", to: Routes.group_task_path(@conn, :show, @group_task), - class: "btn btn-link cb-text ml-auto" + class: "cb-btn cb-btn-link cb-text cb-ml-auto" )}
<% end) %> diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/index.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/index.html.heex index aa504acc7..29d05423f 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/index.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/index.html.heex @@ -1,46 +1,45 @@ -
-

Group Tasks

+
+

Group Tasks

{link("Create new group task", to: Routes.group_task_path(@conn, :new), - class: "btn btn-success text-white mt-2 cb-rounded" + class: "cb-btn cb-btn-success cb-text-white cb-mt-2 cb-rounded" )} -
-
{gettext("Place")}{gettext("Score")}{gettext("Clan players count")}{gettext("Clan")}{gettext("Place")}{gettext("Score")}{gettext("Clan players count")}{gettext("Clan")}
+
{item.place} + {item.score} + {item.players_count} + {item.clan_name}
+
+
- - - - + + + + <%= for group_task <- @group_tasks do %> - - - + + - - diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/new.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/new.html.heex index ac95bb726..07609e611 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/new.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/new.html.heex @@ -1,4 +1,4 @@ -
-

New Group Task

+
+

New Group Task

{render("_form.html", assigns |> Map.put(:action, Routes.group_task_path(@conn, :create)))}
diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/show.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/show.html.heex index 1946a4c12..cf05b05f5 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_task/show.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_task/show.html.heex @@ -1,15 +1,15 @@ -
-

+
+

{@group_task.slug}

-
-
-
+
+
+

ID: {@group_task.id}

Slug: {@group_task.slug}

-
+

Time To Solve: {@group_task.time_to_solve_sec} sec

Created At: {format_datetime(@group_task.inserted_at)}

Runner URL: {@group_task.runner_url || "none"}

@@ -17,35 +17,37 @@
-
-
-

Latest Solutions

+
+
+

Latest Solutions

showing {length(@solutions)} most recent
-
-

idslugtime_to_solve_secinserted_atidslugtime_to_solve_secinserted_at
{group_task.id}{group_task.slug} + {group_task.id}{group_task.slug} {group_task.time_to_solve_sec} + {format_datetime(group_task.inserted_at)} -
+
+
{link("Show", to: Routes.group_task_path(@conn, :show, group_task), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded" )} {link("Edit", to: Routes.group_task_path(@conn, :edit, group_task), - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} {link("Delete", to: Routes.group_task_path(@conn, :delete, group_task), method: :delete, data: [confirm: "Are you sure you want to delete this group task?"], - class: "btn btn-sm btn-outline-danger cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-danger cb-rounded cb-ml-2" )}
+
+
- - - - - - + + + + + + <%= for solution <- @solutions do %> - - - - - - + + + + @@ -76,14 +78,14 @@ -
+
{link("Edit", to: Routes.group_task_path(@conn, :edit, @group_task), - class: "btn btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-outline-secondary cb-rounded" )} {link("Back", to: Routes.group_task_path(@conn, :index), - class: "btn btn-link cb-text ml-auto" + class: "cb-btn cb-btn-link cb-text cb-ml-auto" )}
diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/_form.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/_form.html.heex index 726f6c6cf..a8522c656 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/_form.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/_form.html.heex @@ -1,103 +1,103 @@ -<%= form_for(@changeset, @action, [class: "col-8 offset-2"], fn f -> %> -
-
Name
+<%= form_for(@changeset, @action, [class: "cb-col-8 cb-offset-2"], fn f -> %> +
+
Name
{text_input(f, :name, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", maxlength: "255", required: true )} {error_tag(f, :name)}
-
-
Slug
+
+
Slug
{text_input(f, :slug, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", maxlength: "255", required: true )} {error_tag(f, :slug)}
-
-
Description
+
+
Description
{textarea(f, :description, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", rows: 6, required: true )} {error_tag(f, :description)}
-
+
- + Task Description (optional — shown as a separate tab next to Description)
{textarea(f, :task_description, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", rows: 10 )} {error_tag(f, :task_description)}
-
-
Group Task
+
+
Group Task
{select(f, :group_task_id, Enum.map(@group_tasks, &{&1.slug, &1.id}), - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", required: true, prompt: "Select group task" )} {error_tag(f, :group_task_id)}
-
-
Starts At (UTC)
+
+
Starts At (UTC)
{datetime_local_input(f, :starts_at, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", required: true )} {error_tag(f, :starts_at)}
-
-
Rounds Count
+
+
Rounds Count
{number_input(f, :rounds_count, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1", required: true )} {error_tag(f, :rounds_count)}
-
-
Round Timeout (sec)
+
+
Round Timeout (sec)
{number_input(f, :round_timeout_seconds, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1", required: true )} {error_tag(f, :round_timeout_seconds)}
-
+
- + Seed Round Timeout (sec) — optional. Used only when has_seed_round is on and round 1 is the seed round; leave blank to reuse the round timeout above.
{number_input(f, :seed_round_timeout_seconds, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1" )} {error_tag(f, :seed_round_timeout_seconds)}
-
+
- + Local Folder (optional) — folder name under the player's Downloads directory. When set, the "Add Solution +" button opens that folder in VS Code (vscode://file/…/Downloads/<folder>) instead of @@ -105,287 +105,287 @@
{text_input(f, :local_folder, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", maxlength: "255" )} {error_tag(f, :local_folder)}
-
-
Max Score
+
+
Max Score
{number_input(f, :max_score, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1" )} {error_tag(f, :max_score)}
-

Format

+

Format

-
-
Type
+
+
Type
{select(f, :type, Enum.map(Codebattle.GroupTournament.types(), &{&1, &1}), - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white" + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white" )} {error_tag(f, :type)}
-
-
Slice Size (players per slice group)
+
+
Slice Size (players per slice group)
{number_input(f, :slice_size, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1" )} {error_tag(f, :slice_size)}
-
+
- + Slice Count (leave blank to derive from players / slice size)
{number_input(f, :slice_count, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1" )} {error_tag(f, :slice_count)}
-
-
Slice Strategy
+
+
Slice Strategy
{select( f, :slice_strategy, Enum.map(Codebattle.GroupTournament.slice_strategies(), &{&1, &1}), - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white" + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white" )} {error_tag(f, :slice_strategy)}
-
-
Scoring Strategy
+
+
Scoring Strategy
{select( f, :scoring_strategy, Enum.map(Codebattle.GroupTournament.scoring_strategies(), &{&1, &1}), - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white" + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white" )} {error_tag(f, :scoring_strategy)}
-
-
Movement Strategy
+
+
Movement Strategy
{select( f, :movement_strategy, Enum.map(Codebattle.GroupTournament.movement_strategies(), &{&1, &1}), - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white" + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white" )} {error_tag(f, :movement_strategy)}
-
-
Place Weight
+
+
Place Weight
{number_input(f, :place_weight, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "1" )} {error_tag(f, :place_weight)}
-
-
Break Between Rounds (sec)
+
+
Break Between Rounds (sec)
{number_input(f, :break_duration_seconds, - class: "form-control form-control-lg cb-bg-panel cb-border-color text-white", + class: "cb-form-control cb-form-control-lg cb-bg-panel cb-border-color cb-text-white", min: "0" )} {error_tag(f, :break_duration_seconds)}
-
- {checkbox(f, :include_bots, class: "form-check-input")} -
inserted_atuser_iduserlangsolutioninserted_atuser_iduserlangsolution
{format_datetime(solution.inserted_at)}{solution.user_id}{solution.user && solution.user.name}{solution.lang} -
{solution.solution}
+
+ {format_datetime(solution.inserted_at)} + {solution.user_id}{solution.user && solution.user.name}{solution.lang} +
{solution.solution}
+
{link("Edit", to: Routes.edit_group_task_solution_path( @@ -54,7 +56,7 @@ @group_task, solution.id ), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded" )} {link("Delete", to: @@ -66,7 +68,7 @@ ), method: :delete, data: [confirm: "Are you sure you want to delete this solution?"], - class: "btn btn-sm btn-outline-danger cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-danger cb-rounded cb-ml-2" )}
+
+
- - - - - - - + + + + + + - <%= for group_tournament <- @group_tournaments do %> - - - - + + + - - + - - - diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/leaderboard.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/leaderboard.html.heex index 3924017a6..61ea707c5 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/leaderboard.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/leaderboard.html.heex @@ -29,8 +29,8 @@ end tab_class = fn active -> if active, - do: "btn btn-sm btn-light text-dark font-weight-bold mr-2 mb-2", - else: "btn btn-sm btn-outline-light mr-2 mb-2" + do: "cb-btn cb-btn-sm cb-btn-light cb-text-dark cb-fw-bold cb-mr-2 cb-mb-2", + else: "cb-btn cb-btn-sm cb-btn-outline-light cb-mr-2 cb-mb-2" end slices_for_round = fn round_number -> @@ -69,31 +69,31 @@ end %> -
-
-

Leaderboard — {@group_tournament.name}

+
+
+

Leaderboard — {@group_tournament.name}

{link("Back", to: Routes.admin_group_tournament_path(@conn, :show, @group_tournament), - class: "btn btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-outline-secondary cb-rounded" )}
-
-
State: {@group_tournament.state}
-
+
+
State: {@group_tournament.state}
+
Round: {@group_tournament.current_round_position}/{rounds_count}
-
Players: {@group_tournament.players_count}
-
+
Players: {@group_tournament.players_count}
+
Visible to players: {inspect(@group_tournament.show_leaderboard)}
-
auto-refresh every 5s
+
auto-refresh every 5s
-
-
+
+
{link("Rating", to: view_path.(:rating), class: tab_class.(@view == :rating))} <%= for r <- rounds do %> {link(round_label.(r), @@ -106,39 +106,39 @@ end %> <%= cond do %> <% Enum.empty?(@leaderboard) -> %> -
+
No leaderboard data yet.
<% @view == :rating -> %> -
-
-
idnamesluggroup_taskstateplayers + idnamesluggroup_taskstateplayers starts_at{sort_arrow(@sort_by, @sort_dir, :starts_at)} + finished_at{sort_arrow(@sort_by, @sort_dir, :finished_at)} @@ -45,44 +45,45 @@
{group_tournament.id}{group_tournament.name}{group_tournament.slug} + {group_tournament.id}{group_tournament.name}{group_tournament.slug} {group_tournament.group_task && group_tournament.group_task.slug} {group_tournament.state} + + {group_tournament.state} + {group_tournament.players_count} + {format_datetime(group_tournament.starts_at)} + {format_datetime(group_tournament.finished_at)} -
+
+
{link("Show", to: Routes.admin_group_tournament_path(@conn, :show, group_tournament), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded" )} {link("Edit", to: Routes.admin_group_tournament_path(@conn, :edit, group_tournament), - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} {link("Start", to: Routes.admin_group_tournament_path(@conn, :start, group_tournament), method: :post, data: [confirm: "Start this group tournament now?"], - class: "btn btn-sm btn-success text-white cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-success cb-text-white cb-rounded cb-ml-2" )} {link("Delete", to: Routes.admin_group_tournament_path(@conn, :delete, group_tournament), method: :delete, data: [confirm: "Are you sure you want to delete this group tournament?"], - class: "btn btn-sm btn-outline-danger cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-danger cb-rounded cb-ml-2" )}
+
+
+
- - - - + + + + <%= for r <- rounds do %> - + <% end %> - + <%= for {entry, index} <- Enum.with_index(@leaderboard, 1) do %> - - - + + - - + <%= for r <- rounds do %> <% cell = cell_for.(entry, r) %> - <% end %> - @@ -171,36 +171,38 @@ end %> <% {:round, round_number} = @view %> <% slices = slices_for_round.(round_number) %> <%= if slices == [] do %> -
+
No data yet for {round_label.(round_number)}.
<% else %> -
+
<%= for {slice_index, players} <- slices do %>
-
- Group {slice_index + 1} - {length(players)} players +
+ Group {slice_index + 1} + {length(players)} players
-
#PlayerClanSlice#PlayerClanSlice{round_label.(r)}{round_label.(r)}TotalTotal
{index} +
{index} {entry[:name] || "##{entry[:user_id]}"} <%= if entry[:state] == "left" do %> - Left + Left <% end %> {entry[:clan] || "—"} + {entry[:clan] || "—"} <%= if is_integer(entry[:slice_index]) do %> {entry[:slice_index] + 1} <% else %> @@ -147,18 +147,18 @@ end %> + <%= if cell do %> - {cell[:score] || 0} + {cell[:score] || 0} <%= if is_integer(cell[:place]) do %> - (#{cell[:place]}) + (#{cell[:place]}) <% end %> <% else %> - + <% end %> + {entry[:total_score] || 0}
+
- - - - + + + + <%= for {p, idx} <- Enum.with_index(players, 1) do %> - - - - - + + + + diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/new.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/new.html.heex index f76888100..b70b845ed 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/new.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/new.html.heex @@ -1,5 +1,5 @@ -
-

New Group Tournament

+
+

New Group Tournament

{render( "_form.html", assigns diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/show.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/show.html.heex index 3954fda25..68a55b98f 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/show.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/show.html.heex @@ -1,17 +1,17 @@ -
-

+
+

{@group_tournament.name}

-
-
-
+
+
+

ID: {@group_tournament.id}

Name: {@group_tournament.name}

Slug: {@group_tournament.slug}

State: {@group_tournament.state}

-
+

Group Task: {@group_tournament.group_task && @group_tournament.group_task.slug} @@ -41,14 +41,14 @@

-
-
+
+
<%= if @group_tournament.state == "waiting_participants" do %> {link("Start Tournament", to: Routes.admin_group_tournament_path(@conn, :start, @group_tournament), method: :post, data: [confirm: "Start this group tournament now?"], - class: "btn btn-success text-white cb-rounded" + class: "cb-btn cb-btn-success cb-text-white cb-rounded" )} {link("Start 5-min Timer", to: Routes.admin_group_tournament_path(@conn, :start_timer, @group_tournament), @@ -57,7 +57,7 @@ confirm: "Show a 5-minute countdown to all participants and auto-start the tournament when it elapses?" ], - class: "btn btn-outline-warning cb-rounded ml-2" + class: "cb-btn cb-btn-outline-warning cb-rounded cb-ml-2" )} {link("Smooth Start (60s)", to: Routes.admin_group_tournament_path(@conn, :smooth_start, @group_tournament), @@ -66,7 +66,7 @@ confirm: "Start the tournament now but spread the status_updated push to players evenly over 60 seconds?" ], - class: "btn btn-outline-info cb-rounded ml-2" + class: "cb-btn cb-btn-outline-info cb-rounded cb-ml-2" )} <% end %> <%= if @group_tournament.state == "active" do %> @@ -74,7 +74,7 @@ to: Routes.admin_group_tournament_path(@conn, :finish, @group_tournament), method: :post, data: [confirm: "Finish this group tournament now?"], - class: "btn btn-success text-white cb-rounded" + class: "cb-btn cb-btn-success cb-text-white cb-rounded" )} {link("Force Finish Break", to: Routes.admin_group_tournament_path(@conn, :force_finish_break, @group_tournament), @@ -82,20 +82,20 @@ data: [ confirm: "End the current break and start the next round now with the same slices?" ], - class: "btn btn-outline-warning cb-rounded ml-2" + class: "cb-btn cb-btn-outline-warning cb-rounded cb-ml-2" )} <% end %> {link("Cancel", to: Routes.admin_group_tournament_path(@conn, :cancel, @group_tournament), method: :post, data: [confirm: "Cancel this group tournament?"], - class: "btn btn-outline-danger cb-rounded ml-2" + class: "cb-btn cb-btn-outline-danger cb-rounded cb-ml-2" )} {link("Restart", to: Routes.admin_group_tournament_path(@conn, :reset, @group_tournament), method: :post, data: [confirm: "Reset players, runs, and solutions for this group tournament?"], - class: "btn btn-outline-warning cb-rounded ml-2" + class: "cb-btn cb-btn-outline-warning cb-rounded cb-ml-2" )} {link("Retry", to: Routes.admin_group_tournament_path(@conn, :retry, @group_tournament), @@ -104,22 +104,22 @@ confirm: "Keep all players, clear their scores/runs/solutions, and move tournament back to waiting participants?" ], - class: "btn btn-outline-info cb-rounded ml-2" + class: "cb-btn cb-btn-outline-info cb-rounded cb-ml-2" )}
-
+
{link("Open Group Tournament", to: Routes.group_tournament_path(@conn, :show, @group_tournament), target: "_blank", rel: "noopener noreferrer", - class: "btn btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-outline-secondary cb-rounded" )} {link("Run Checker", to: Routes.admin_group_tournament_path(@conn, :check, @group_tournament), method: :post, data: [confirm: "Run checker for the tournament players now?"], - class: "btn btn-outline-success cb-rounded ml-2" + class: "cb-btn cb-btn-outline-success cb-rounded cb-ml-2" )} {link("Run All Slices", to: Routes.admin_group_tournament_path(@conn, :run_all_slices, @group_tournament), @@ -128,7 +128,7 @@ confirm: "Run every slice synchronously now? This calls the runner for each slice and may take a while." ], - class: "btn btn-outline-warning cb-rounded ml-2" + class: "cb-btn cb-btn-outline-warning cb-rounded cb-ml-2" )} {link("Run Top 8", to: Routes.admin_group_tournament_path(@conn, :run_top8, @group_tournament), @@ -137,15 +137,15 @@ confirm: "Run a top-8 slice now? Picks the best solution per player (highest score, earliest submission as tiebreak), takes the top 8, and runs them together as one slice." ], - class: "btn btn-outline-warning cb-rounded ml-2" + class: "cb-btn cb-btn-outline-warning cb-rounded cb-ml-2" )} {link("Solution Tester", to: Routes.admin_group_tournament_path(@conn, :solution_tester, @group_tournament), - class: "btn btn-outline-info cb-rounded ml-2" + class: "cb-btn cb-btn-outline-info cb-rounded cb-ml-2" )} {link("Leaderboard", to: Routes.admin_group_tournament_path(@conn, :leaderboard, @group_tournament), - class: "btn btn-outline-info cb-rounded ml-2" + class: "cb-btn cb-btn-outline-info cb-rounded cb-ml-2" )} {link( if(@group_tournament.show_leaderboard, @@ -163,8 +163,8 @@ ], class: if(@group_tournament.show_leaderboard, - do: "btn btn-outline-warning cb-rounded ml-2", - else: "btn btn-outline-success cb-rounded ml-2" + do: "cb-btn cb-btn-outline-warning cb-rounded cb-ml-2", + else: "cb-btn cb-btn-outline-success cb-rounded cb-ml-2" ) )} {link( @@ -184,8 +184,8 @@ ], class: if(@group_tournament.visible_to_users, - do: "btn btn-outline-warning cb-rounded ml-2", - else: "btn btn-outline-success cb-rounded ml-2" + do: "cb-btn cb-btn-outline-warning cb-rounded cb-ml-2", + else: "cb-btn cb-btn-outline-success cb-rounded cb-ml-2" ) )} {link("Gather Players Here", @@ -195,19 +195,19 @@ confirm: "Force-redirect every non-admin user connected to the main channel to this tournament right now?" ], - class: "btn btn-outline-danger cb-rounded ml-2" + class: "cb-btn cb-btn-outline-danger cb-rounded cb-ml-2" )}
-
-

Generate Token

+
+

Generate Token

<%= form_for(@token_changeset, Routes.admin_group_tournament_path(@conn, :create_token, @group_tournament), [as: :group_tournament_token], fn f -> %> -
- User ID +
+ User ID {number_input(f, :user_id, - class: "form-control cb-bg-panel cb-border-color text-white mt-2", + class: "cb-form-control cb-bg-panel cb-border-color cb-text-white cb-mt-2", min: "1", required: true )} @@ -215,37 +215,37 @@
{submit("Generate / Rotate Token", phx_disable_with: "Saving...", - class: "btn btn-success text-white cb-rounded" + class: "cb-btn cb-btn-success cb-text-white cb-rounded" )} <% end) %>
-
-

Add User to Tournament

+
+

Add User to Tournament

-
- User ID +
+ User ID
-
-
-

Bulk Add Users

-

+

+

Bulk Add Users

+

Paste user IDs (comma, space, or newline separated). For each existing user, the same add-user flow runs as the single "Add User" form above: the user is linked to the tournament and a player record is created/updated with lang: "js" @@ -256,19 +256,19 @@ method="post" > -

- +
+
-
-

Bulk External Operations

-

+

+

Bulk External Operations

+

All operations are dispatched through Oban with per-user retries (max_attempts: 5). Each job is independent — a failure for one user does not block the rest, and successes are not re-run. Jobs are scheduled in batches of @@ -286,9 +286,9 @@ (default 50, max 100) to stay under the external service's 100 RPS rate limit.

-
-

Setup (repo + dev role + secret)

-

+

+

Setup (repo + dev role + secret)

+

Paste user IDs (comma, space, or newline separated). For each user an ExternalSetupWorker job is enqueued that idempotently creates the repo from the tournament template, @@ -302,31 +302,31 @@ method="post" > -

- +
+
-
- +
+ External service is rate-limited to 100 RPS.
-
-

Release Seats

-

+

+

Release Seats

+

Enqueue one SeatReleaseWorker per tournament user. Releases the code-assist seat — skips users whose release_state @@ -453,23 +453,23 @@
-

- +
+
-
-

Remove Dev Roles

-

+

+

Remove Dev Roles

+

Enqueue one DevRoleRemoveWorker per tournament user. Removes the developer role on each user's repo. Skips users whose dev_role_removal_state @@ -488,23 +488,23 @@
-

- +
+
-
-

Add Viewer Roles

-

+

+

Add Viewer Roles

+

Enqueue one ViewerRoleAddWorker per tournament user. Grants the viewer role on each user's repo. Skips users whose viewer_role_state @@ -523,23 +523,23 @@
-

- +
+
-
-

Delete Repos

-

+

+

Delete Repos

+

Enqueue one RepoDeleteWorker - per tournament user. Destructive + per tournament user. Destructive — each job calls DELETE /repos/{"{org}/{repo}"} with silent=true.

-
- +
+
-
-
-

Tournament Users

+
+
+

Tournament Users

{length(@tournament_users)} users with access
-
-

#PlayerClanScore#PlayerClanScore
{p.place || idx}{p.name}{p.clan || "—"} +
{p.place || idx} + {p.name} + {p.clan || "—"} {p.score}
+
+
- - - - - + + + + + @@ -605,13 +605,13 @@ - - - - + + + + <% end %> @@ -619,9 +619,9 @@ -
-
-

+
+
+

Players <%= if not is_nil(@slice_filter) do %> — filtered to {slice_label(@slice_filter)} @@ -633,16 +633,16 @@ <% end %>

-
-

user_iduserstatetokenadded_atuser_iduserstatetokenadded_at
{link(record.user_id, to: "/admin/users/#{record.user_id}", - class: "text-white" + class: "cb-text-white" )} {record.user && record.user.name}{record.state}{record.token}{format_datetime(record.inserted_at)}{record.user && record.user.name}{record.state}{record.token}{format_datetime(record.inserted_at)}
+
+
- - - - - - + + + + + + @@ -651,72 +651,74 @@ - - + - - - + + + <% end %>
user_iduserslicelangstatelast_setup_atuser_iduserslicelangstatelast_setup_at
{link(player.user_id, to: "/admin/users/#{player.user_id}", - class: "text-white" + class: "cb-text-white" )} {player.user && player.user.name} + {player.user && player.user.name} {if is_nil(player.slice_index), do: "-", else: player.slice_index} {player.lang}{player.state}{format_datetime(player.last_setup_at)}{player.lang}{player.state} + {format_datetime(player.last_setup_at)} +
<%= if @players_pages > 1 do %> -
+
<%= if @players_page > 1 do %> {link("« prev", to: show_path(@conn, @group_tournament, %{players_page: @players_page - 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded mr-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-mr-2" )} <% end %> - page {@players_page} of {@players_pages} + page {@players_page} of {@players_pages} <%= if @players_page < @players_pages do %> {link("next »", to: show_path(@conn, @group_tournament, %{players_page: @players_page + 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} <% end %>
<% end %>
-
-
+
+
-

Slice Runs

+

Slice Runs

scored runs — one per slice on each timer tick
{@slice_runs_total} total — page {@slice_runs_page} of {@slice_runs_pages}
-
- +
+
- - - - - + + + + + <%= for run <- @slice_runs do %> - - - - - + + + + @@ -766,56 +765,56 @@
inserted_atslicestatusplayer_idsdownloadsinserted_atslicestatusplayer_idsdownloads
{format_datetime(run.inserted_at)}{run.slice_index}{run.status}{Enum.join(run.player_ids, ", ")} + {format_datetime(run.inserted_at)}{run.slice_index}{run.status}{Enum.join(run.player_ids, ", ")} <%= cond do %> <% run.status == "error" -> %> - {extract_run_error(run.result)} + {extract_run_error(run.result)} <% run.status == "pending" -> %> nil <% true -> %> @@ -731,8 +733,7 @@ ), target: "_blank", rel: "noopener noreferrer", - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded" )} {link("History JSON", to: @@ -743,8 +744,7 @@ run.id, "history" ), - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} {link("Summary JSON", to: @@ -755,8 +755,7 @@ run.id, "summary" ), - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} <% end %>
<%= if @slice_runs_pages > 1 do %> -
+
<%= if @slice_runs_page > 1 do %> {link("« prev", to: show_path(@conn, @group_tournament, %{slice_runs_page: @slice_runs_page - 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded mr-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-mr-2" )} <% end %> - page {@slice_runs_page} of {@slice_runs_pages} + page {@slice_runs_page} of {@slice_runs_pages} <%= if @slice_runs_page < @slice_runs_pages do %> {link("next »", to: show_path(@conn, @group_tournament, %{slice_runs_page: @slice_runs_page + 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} <% end %>
<% end %>
-
-
+
+
-

Latest Runs

+

Latest Runs

per-submission preview runs — single user, not scored
{@runs_total} total — page {@runs_page} of {@runs_pages}
-
- +
+
- - - - - + + + + + <%= for run <- @runs do %> - - - - - + + + + @@ -865,60 +861,62 @@
inserted_atstatusscoreplayer_idsdownloadsinserted_atstatusscoreplayer_idsdownloads
{format_datetime(run.inserted_at)}{run.status}{run.score}{Enum.join(run.player_ids, ", ")} + {format_datetime(run.inserted_at)}{run.status}{run.score}{Enum.join(run.player_ids, ", ")} <%= cond do %> <% run.status == "error" -> %> - {extract_run_error(run.result)} + {extract_run_error(run.result)} <% run.status == "pending" -> %> nil <% true -> %> @@ -830,8 +829,7 @@ ), target: "_blank", rel: "noopener noreferrer", - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded" )} {link("History JSON", to: @@ -842,8 +840,7 @@ run.id, "history" ), - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} {link("Summary JSON", to: @@ -854,8 +851,7 @@ run.id, "summary" ), - class: - "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} <% end %>
<%= if @runs_pages > 1 do %> -
+
<%= if @runs_page > 1 do %> {link("« prev", to: show_path(@conn, @group_tournament, %{runs_page: @runs_page - 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded mr-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-mr-2" )} <% end %> - page {@runs_page} of {@runs_pages} + page {@runs_page} of {@runs_pages} <%= if @runs_page < @runs_pages do %> {link("next »", to: show_path(@conn, @group_tournament, %{runs_page: @runs_page + 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} <% end %>
<% end %>
-
-
-

Latest Solutions

+
+
+

Latest Solutions

{@solutions_total} total — page {@solutions_page} of {@solutions_pages}
-
- +
+
- - - - - - + + + + + + <%= for solution <- @solutions do %> - + - - - + + - @@ -948,32 +946,32 @@
inserted_atuser_iduserlangsolutioninserted_atuser_iduserlangsolution
{format_datetime(solution.inserted_at)} + {format_datetime(solution.inserted_at)} + {link(solution.user_id, to: "/admin/users/#{solution.user_id}", - class: "text-white" + class: "cb-text-white" )} {solution.user && solution.user.name}{solution.lang} -
{solution.solution}
+
{solution.user && solution.user.name}{solution.lang} +
{solution.solution}
+ {link("Edit", to: Routes.edit_group_task_solution_path( @@ -927,7 +925,7 @@ @group_tournament.group_task, solution.id ), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded" )} {link("Delete", to: @@ -939,7 +937,7 @@ ), method: :delete, data: [confirm: "Are you sure you want to delete this solution?"], - class: "btn btn-sm btn-outline-danger cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-danger cb-rounded cb-ml-2" )}
<%= if @solutions_pages > 1 do %> -
+
<%= if @solutions_page > 1 do %> {link("« prev", to: show_path(@conn, @group_tournament, %{solutions_page: @solutions_page - 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded mr-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-mr-2" )} <% end %> - page {@solutions_page} of {@solutions_pages} + page {@solutions_page} of {@solutions_pages} <%= if @solutions_page < @solutions_pages do %> {link("next »", to: show_path(@conn, @group_tournament, %{solutions_page: @solutions_page + 1}), - class: "btn btn-sm btn-outline-secondary cb-btn-outline-secondary cb-rounded ml-2" + class: "cb-btn cb-btn-sm cb-btn-outline-secondary cb-rounded cb-ml-2" )} <% end %>
<% end %>
-
+
{link("Edit", to: Routes.admin_group_tournament_path(@conn, :edit, @group_tournament), - class: "btn btn-outline-secondary cb-btn-outline-secondary cb-rounded" + class: "cb-btn cb-btn-outline-secondary cb-rounded" )} {link("Back", to: Routes.admin_group_tournament_path(@conn, :index), - class: "btn btn-link cb-text ml-auto" + class: "cb-btn cb-btn-link cb-text cb-ml-auto" )}
diff --git a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/solution_tester.html.heex b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/solution_tester.html.heex index d807c3b0c..94e51e879 100644 --- a/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/solution_tester.html.heex +++ b/apps/codebattle/lib/codebattle_web/templates/admin/group_tournament/solution_tester.html.heex @@ -1,23 +1,26 @@ -
-

+
+

Solution Tester — {@group_tournament.name}

-
+ -
-
-
-
- - <%= for token <- @tokens do %>
-
- +
+
-
- +
+
-
-
-