UI: единый Switch для булевых опций вместо смеси чекбоксов и свитчей
Новый компонент Switch (разметка .switch из global.css). Применён: - создание группы: дополнения теперь свитчи, как в настройках группы; - настройки группы: базовые дополнения показываются, как при создании (включены, заблокированы, подпись «всегда включено»), а не скрываются; - админка: «Группировать по группам» и флаг «выбыл» в правке партии. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { ApiError } from "../api/client";
|
|||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../context/ToastContext";
|
||||||
import { useCreateGroup } from "../hooks/groups";
|
import { useCreateGroup } from "../hooks/groups";
|
||||||
import { useExpansions } from "../hooks/reference";
|
import { useExpansions } from "../hooks/reference";
|
||||||
|
import { Switch } from "./Switch";
|
||||||
|
|
||||||
export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
|
export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
|
||||||
const { data: expansions } = useExpansions();
|
const { data: expansions } = useExpansions();
|
||||||
@@ -47,9 +48,7 @@ export function CreateGroupForm({ onCreated }: { onCreated?: () => void }) {
|
|||||||
{e.name_ru}
|
{e.name_ru}
|
||||||
{e.is_base && <span className="muted small"> (всегда включено)</span>}
|
{e.is_base && <span className="muted small"> (всегда включено)</span>}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<Switch
|
||||||
type="checkbox"
|
|
||||||
style={{ width: "auto" }}
|
|
||||||
disabled={e.is_base}
|
disabled={e.is_base}
|
||||||
checked={e.is_base || selected.has(e.id)}
|
checked={e.is_base || selected.has(e.id)}
|
||||||
onChange={() => toggle(e.id)}
|
onChange={() => toggle(e.id)}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Тумблер вкл/выкл (стили .switch в global.css) — единый контрол для булевых опций
|
||||||
|
* (дополнения группы, фильтры, флаги). Вместо нативных чекбоксов.
|
||||||
|
*/
|
||||||
|
export function Switch({
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span className="switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onChange?.(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="switch-track">
|
||||||
|
<span className="switch-thumb" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { ApiError } from "../api/client";
|
import { ApiError } from "../api/client";
|
||||||
import { PlayerLink } from "../components/PlayerLink";
|
import { PlayerLink } from "../components/PlayerLink";
|
||||||
import { Spinner } from "../components/Spinner";
|
import { Spinner } from "../components/Spinner";
|
||||||
|
import { Switch } from "../components/Switch";
|
||||||
import { MAX_GROUP_SIZE } from "../domain/constants";
|
import { MAX_GROUP_SIZE } from "../domain/constants";
|
||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../context/ToastContext";
|
||||||
import { useMe } from "../hooks/auth";
|
import { useMe } from "../hooks/auth";
|
||||||
@@ -73,24 +74,20 @@ export function GroupSettingsPage() {
|
|||||||
<div className="card">
|
<div className="card">
|
||||||
<h3>Дополнения</h3>
|
<h3>Дополнения</h3>
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
{(expansions ?? [])
|
{/* Базовые дополнения показываем, как и при создании группы: включены и заблокированы. */}
|
||||||
.filter((e) => !e.is_base)
|
{(expansions ?? []).map((e) => (
|
||||||
.map((e) => (
|
<label key={e.id} className="row" style={{ justifyContent: "space-between" }}>
|
||||||
<label key={e.id} className="row" style={{ justifyContent: "space-between" }}>
|
<span>
|
||||||
<span>{e.name_ru}</span>
|
{e.name_ru}
|
||||||
<span className="switch">
|
{e.is_base && <span className="muted small"> (всегда включено)</span>}
|
||||||
<input
|
</span>
|
||||||
type="checkbox"
|
<Switch
|
||||||
disabled={!isOwner}
|
disabled={e.is_base || !isOwner}
|
||||||
checked={selected.has(e.id)}
|
checked={e.is_base || selected.has(e.id)}
|
||||||
onChange={() => toggle(e.id)}
|
onChange={() => toggle(e.id)}
|
||||||
/>
|
/>
|
||||||
<span className="switch-track">
|
</label>
|
||||||
<span className="switch-thumb" />
|
))}
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<button className="btn btn-primary" style={{ marginTop: 10 }} onClick={saveExpansions}>
|
<button className="btn btn-primary" style={{ marginTop: 10 }} onClick={saveExpansions}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { ApiError } from "../../api/client";
|
import { ApiError } from "../../api/client";
|
||||||
import { MatchMedia } from "../../components/MatchMedia";
|
import { MatchMedia } from "../../components/MatchMedia";
|
||||||
import { Spinner } from "../../components/Spinner";
|
import { Spinner } from "../../components/Spinner";
|
||||||
|
import { Switch } from "../../components/Switch";
|
||||||
import { WIN_REASONS, type WinReason } from "../../domain/winReasons";
|
import { WIN_REASONS, type WinReason } from "../../domain/winReasons";
|
||||||
import { useToast } from "../../context/ToastContext";
|
import { useToast } from "../../context/ToastContext";
|
||||||
import {
|
import {
|
||||||
@@ -141,12 +142,7 @@ export function AdminMatchEdit({ matchId, onClose }: { matchId: number; onClose:
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="row small" style={{ flex: "0 0 auto", gap: 4 }}>
|
<label className="row small" style={{ flex: "0 0 auto", gap: 4 }}>
|
||||||
<input
|
<Switch checked={r.eliminated} onChange={(v) => upd(i, { eliminated: v })} />
|
||||||
type="checkbox"
|
|
||||||
style={{ width: "auto" }}
|
|
||||||
checked={r.eliminated}
|
|
||||||
onChange={(e) => upd(i, { eliminated: e.target.checked })}
|
|
||||||
/>
|
|
||||||
выбыл
|
выбыл
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Pencil, Trash2 } from "lucide-react";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { Spinner } from "../../components/Spinner";
|
import { Spinner } from "../../components/Spinner";
|
||||||
|
import { Switch } from "../../components/Switch";
|
||||||
import { useToast } from "../../context/ToastContext";
|
import { useToast } from "../../context/ToastContext";
|
||||||
import {
|
import {
|
||||||
useAdminDeleteMatch,
|
useAdminDeleteMatch,
|
||||||
@@ -112,12 +113,7 @@ export function AdminRecordsPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<label className="row" style={{ gap: 8 }}>
|
<label className="row" style={{ gap: 8 }}>
|
||||||
<input
|
<Switch checked={grouped} onChange={setGrouped} />
|
||||||
type="checkbox"
|
|
||||||
style={{ width: "auto" }}
|
|
||||||
checked={grouped}
|
|
||||||
onChange={(e) => setGrouped(e.target.checked)}
|
|
||||||
/>
|
|
||||||
Группировать по группам
|
Группировать по группам
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user