diff --git a/backend/app/routers/groups.py b/backend/app/routers/groups.py
index d4908cb..80ee642 100644
--- a/backend/app/routers/groups.py
+++ b/backend/app/routers/groups.py
@@ -17,6 +17,7 @@ from app.services import (
notification_service,
notify,
stats_service,
+ user_service,
)
router = APIRouter(prefix="/groups", tags=["groups"])
@@ -127,7 +128,12 @@ def list_members(
) -> list[s.MemberRead]:
group_service.assert_member(session, group_id, user.id) # type: ignore[arg-type]
return [
- s.MemberRead(user_id=u.id, nickname=u.nickname, role=m.role)
+ s.MemberRead(
+ user_id=u.id,
+ nickname=u.nickname,
+ role=m.role,
+ avatar_url=user_service.avatar_url_for(u.id, u.avatar_path, u.updated_at),
+ )
for m, u in membership_service.list_members(session, group_id)
]
@@ -195,10 +201,13 @@ def change_member_role(
group_service.assert_owner(session, group_id, user.id) # type: ignore[arg-type]
group = group_service.get_group(session, group_id)
member = membership_service.change_role(session, group, user_id, body.role)
- from app.services.user_service import get_user
-
- u = get_user(session, user_id)
- return s.MemberRead(user_id=u.id, nickname=u.nickname, role=member.role)
+ u = user_service.get_user(session, user_id)
+ return s.MemberRead(
+ user_id=u.id,
+ nickname=u.nickname,
+ role=member.role,
+ avatar_url=user_service.avatar_url_for(u.id, u.avatar_path, u.updated_at),
+ )
# ─── Партии и статистика группы ──────────────────────────────────────────────
diff --git a/backend/app/schemas/api.py b/backend/app/schemas/api.py
index 84dcb6b..b8cc7eb 100644
--- a/backend/app/schemas/api.py
+++ b/backend/app/schemas/api.py
@@ -147,6 +147,7 @@ class MemberRead(BaseModel):
user_id: int
nickname: str
role: str
+ avatar_url: str | None = None
class MemberAdd(BaseModel):
diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts
index b3f0f86..dbc5301 100644
--- a/frontend/src/api/schema.d.ts
+++ b/frontend/src/api/schema.d.ts
@@ -1778,6 +1778,8 @@ export interface components {
nickname: string;
/** Role */
role: string;
+ /** Avatar Url */
+ avatar_url?: string | null;
};
/** MemberRoleUpdate */
MemberRoleUpdate: {
diff --git a/frontend/src/components/PlayerSelect.tsx b/frontend/src/components/PlayerSelect.tsx
new file mode 100644
index 0000000..8e4afeb
--- /dev/null
+++ b/frontend/src/components/PlayerSelect.tsx
@@ -0,0 +1,63 @@
+import { ChevronDown } from "lucide-react";
+import { useState } from "react";
+
+import { Avatar } from "./Avatar";
+import { PlayerSuggestions, type PlayerSuggestion } from "./PlayerSuggestions";
+
+/**
+ * Выбор игрока «как select», но в стиле выпадающих подсказок ников:
+ * кнопка-триггер с аватаром и ником, под ней список options. Родитель сам
+ * фильтрует options (например, убирает выбранных в других строках).
+ */
+export function PlayerSelect({
+ value,
+ options,
+ onPick,
+}: {
+ value: number | "";
+ options: PlayerSuggestion[];
+ onPick: (userId: number | "") => void;
+}) {
+ const [open, setOpen] = useState(false);
+ const selected = value === "" ? null : options.find((o) => o.user_id === value) ?? null;
+ return (
+
+
+ {open && (
+
{
+ onPick(u.user_id);
+ setOpen(false);
+ }}
+ onClear={
+ selected
+ ? () => {
+ onPick("");
+ setOpen(false);
+ }
+ : undefined
+ }
+ />
+ )}
+
+ );
+}
diff --git a/frontend/src/components/PlayerSuggestions.tsx b/frontend/src/components/PlayerSuggestions.tsx
index feb078d..1c165ac 100644
--- a/frontend/src/components/PlayerSuggestions.tsx
+++ b/frontend/src/components/PlayerSuggestions.tsx
@@ -14,13 +14,28 @@ export interface PlayerSuggestion {
export function PlayerSuggestions({
items,
onPick,
+ onClear,
}: {
items: PlayerSuggestion[];
onPick: (item: PlayerSuggestion) => void;
+ // Необязательная строка сброса сверху списка («— игрок —» в PlayerSelect).
+ onClear?: () => void;
}) {
- if (items.length === 0) return null;
+ if (items.length === 0 && !onClear) return null;
return (
+ {onClear && (
+
+ )}
{items.map((u) => (
-
+ options={memberOptions
+ .filter((m) => !takenUserIds(idx).includes(m.user_id))
+ .map((m) => ({
+ user_id: m.user_id,
+ nickname: m.nickname,
+ avatar_url: m.avatar_url,
+ }))}
+ onPick={(id) => update(idx, { user_id: id })}
+ />
{spin[idx] != null ? (
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 096faf8..a300f48 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -321,6 +321,29 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
.suggest-row:last-child { border-bottom: none; }
.suggest-row:hover, .suggest-row:active { background: var(--surface-2); }
.suggest-row .nick { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.suggest-clear { color: var(--muted); font-weight: 400; }
+
+/* Кнопка-триггер PlayerSelect: выглядит как select, раскрывает .suggest-list. */
+.picker-trigger {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ min-height: 42px; /* высота обычного input/select — строки формы не «прыгают» */
+ background: var(--surface-2);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 8px 12px;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+ outline: none;
+}
+.picker-trigger:focus { border-color: var(--accent); }
+.picker-trigger .nick { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.picker-placeholder { color: var(--muted); }
+.picker-chevron { margin-left: auto; flex: none; color: var(--muted); }
/* Всплывашки сверху экрана в момент прихода */
.toast-top-wrap {