feat: begin to add support for multiple teams

This commit is contained in:
julius 2025-03-19 15:08:18 +01:00
parent c246a0b264
commit ded2b79db7
Signed by: julius
GPG Key ID: C80A63E6A5FD7092
7 changed files with 91 additions and 42 deletions

2
db.py
View File

@ -51,7 +51,7 @@ class Player(SQLModel, table=True):
disabled: bool | None = None disabled: bool | None = None
hashed_password: str | None = None hashed_password: str | None = None
number: str | None = None number: str | None = None
teams: list[Team] | None = Relationship( teams: list[Team] = Relationship(
back_populates="players", link_model=PlayerTeamLink back_populates="players", link_model=PlayerTeamLink
) )
scopes: str = "" scopes: str = ""

View File

@ -59,10 +59,11 @@ def add_players(players: list[Player]):
session.commit() session.commit()
async def list_players(): async def list_players(team_id: int):
with Session(engine) as session: with Session(engine) as session:
statement = select(Player).order_by(Player.display_name) statement = select(Team).where(Team.id == team_id)
players = session.exec(statement).fetchall() players = [t.players for t in session.exec(statement)][0]
if players:
return [ return [
player.model_dump(include={"id", "display_name", "number"}) player.model_dump(include={"id", "display_name", "number"})
for player in players for player in players

View File

@ -408,6 +408,10 @@ button {
} }
} }
.avatars {
margin: 16px auto;
}
.avatar { .avatar {
font-weight: bold; font-weight: bold;
font-size: 110%; font-size: 110%;
@ -415,8 +419,18 @@ button {
width: fit-content; width: fit-content;
border: 3px solid; border: 3px solid;
border-radius: 1em; border-radius: 1em;
margin: 0 auto 16px auto; margin: 4px auto;
}
.group-avatar {
background-color: aliceblue;
font-weight: bold;
font-size: 90%;
padding: 3px 1em;
width: fit-content;
border: 3px solid;
border-radius: 1em;
margin: 4px auto;
} }
.user-info { .user-info {

View File

@ -1,5 +1,5 @@
import { createRef, MouseEventHandler, useEffect, useState } from "react"; import { createRef, MouseEventHandler, useEffect, useState } from "react";
import { useSession } from "./Session"; import { TeamState, useSession } from "./Session";
import { User } from "./api"; import { User } from "./api";
import { useTheme } from "./ThemeProvider"; import { useTheme } from "./ThemeProvider";
import { colourTheme, darkTheme, normalTheme, rainbowTheme } from "./themes"; import { colourTheme, darkTheme, normalTheme, rainbowTheme } from "./themes";
@ -11,7 +11,7 @@ interface ContextMenuItem {
onClick: () => void; onClick: () => void;
} }
const UserInfo = (user: User, teams: Team[] | undefined) => { const UserInfo = (user: User, teams: TeamState | undefined) => {
return ( return (
<div className="user-info"> <div className="user-info">
<div> <div>
@ -42,9 +42,9 @@ const UserInfo = (user: User, teams: Team[] | undefined) => {
textAlign: "left", textAlign: "left",
}} }}
> >
{teams.map((team) => ( {teams.teams.map((team, index) => (
<li> <li>
{team.name} ( {teams.activeTeam === index ? <b>{team.name}</b> : team.name} (
{team.location || team.country || "location unknown"}) {team.location || team.country || "location unknown"})
</li> </li>
))} ))}
@ -56,7 +56,7 @@ const UserInfo = (user: User, teams: Team[] | undefined) => {
}; };
export default function Avatar() { export default function Avatar() {
const { user, teams, onLogout } = useSession(); const { user, teams, setTeams, onLogout } = useSession();
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const [contextMenu, setContextMenu] = useState<{ const [contextMenu, setContextMenu] = useState<{
@ -147,6 +147,8 @@ export default function Avatar() {
} }
return ( return (
<>
<div className="avatars">
<div <div
className="avatar" className="avatar"
onContextMenu={handleMenuClick} onContextMenu={handleMenuClick}
@ -161,6 +163,24 @@ export default function Avatar() {
ref={avatarRef} ref={avatarRef}
> >
👤 {user?.username} 👤 {user?.username}
</div>
{teams && teams?.teams.length > 1 && (
<select
className="group-avatar"
value={teams.activeTeam}
onChange={(e) =>
setTeams({ ...teams, activeTeam: Number(e.target.value) })
}
>
{teams.teams.map((team) => (
<option key={team.id} value={team.id}>
👥 {team.name}
</option>
))}
</select>
)}
</div>
{contextMenu.open && ( {contextMenu.open && (
<ul <ul
className="context-menu" className="context-menu"
@ -193,6 +213,6 @@ export default function Avatar() {
> >
{dialog} {dialog}
</dialog> </dialog>
</div> </>
); );
} }

View File

@ -277,21 +277,27 @@ function HeaderControl({ onLoad, onClear }: HeaderControlProps) {
} }
export default function Rankings() { export default function Rankings() {
const { user } = useSession(); const { user, teams } = useSession();
const [players, setPlayers] = useState<User[] | null>(null); const [players, setPlayers] = useState<User[] | null>(null);
async function loadPlayers() { async function loadPlayers() {
if (teams) {
try { try {
const data = await apiAuth("player/list", null, "GET"); const data = await apiAuth(
`player/list?team_id=${teams?.activeTeam}`,
null,
"GET"
);
setPlayers(data as User[]); setPlayers(data as User[]);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
} }
}
useEffect(() => { useEffect(() => {
loadPlayers(); loadPlayers();
}, [user]); }, [user, teams]);
const tabs = [ const tabs = [
{ id: "Chemistry", label: "🧪 Chemistry" }, { id: "Chemistry", label: "🧪 Chemistry" },
@ -300,7 +306,7 @@ export default function Rankings() {
return ( return (
<> <>
{user && players ? ( {user && teams && players ? (
<TabController tabs={tabs}> <TabController tabs={tabs}>
<ChemistryDnD {...{ user, players }} /> <ChemistryDnD {...{ user, players }} />
<MVPDnD {...{ user, players }} /> <MVPDnD {...{ user, players }} />

View File

@ -14,15 +14,22 @@ export interface SessionProviderProps {
children: ReactNode; children: ReactNode;
} }
export interface TeamState {
teams: Team[];
activeTeam: number;
}
export interface Session { export interface Session {
user: User | null; user: User | null;
teams: Team[] | null; teams: TeamState | null;
setTeams: (teams: TeamState) => void;
onLogout: () => void; onLogout: () => void;
} }
const sessionContext = createContext<Session>({ const sessionContext = createContext<Session>({
user: null, user: null,
teams: null, teams: null,
setTeams: () => {},
onLogout: () => {}, onLogout: () => {},
}); });
@ -30,7 +37,7 @@ export function SessionProvider(props: SessionProviderProps) {
const { children } = props; const { children } = props;
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [teams, setTeams] = useState<Team[] | null>(null); const [teams, setTeams] = useState<TeamState | null>(null);
const [err, setErr] = useState<unknown>(null); const [err, setErr] = useState<unknown>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -50,12 +57,12 @@ export function SessionProvider(props: SessionProviderProps) {
async function loadTeam() { async function loadTeam() {
const teams: Team[] = await apiAuth("player/me/teams", null, "GET"); const teams: Team[] = await apiAuth("player/me/teams", null, "GET");
if (teams) setTeams(teams); if (teams) setTeams({ teams: teams, activeTeam: teams[0].id });
} }
useEffect(() => { useEffect(() => {
loadUser(); loadUser();
setTimeout(() => loadTeam(), 1500); setTimeout(() => loadTeam(), 500);
}, []); }, []);
function onLogin(user: User) { function onLogin(user: User) {
@ -87,7 +94,7 @@ export function SessionProvider(props: SessionProviderProps) {
content = <Login onLogin={onLogin} />; content = <Login onLogin={onLogin} />;
} else } else
content = ( content = (
<sessionContext.Provider value={{ user, teams, onLogout }}> <sessionContext.Provider value={{ user, teams, setTeams, onLogout }}>
{children} {children}
</sessionContext.Provider> </sessionContext.Provider>
); );

View File

@ -34,6 +34,7 @@ export interface MVPRanking {
} }
export interface Team { export interface Team {
id: number;
name: string; name: string;
location: string; location: string;
country: string; country: string;