feat: towards multi-team support

also testing at different points whether team association is correct
This commit is contained in:
2025-03-21 14:44:55 +01:00
parent 7f4f6142c9
commit b28752830a
10 changed files with 148 additions and 925 deletions

View File

@@ -413,6 +413,7 @@ button {
}
.avatar {
background-color: #f0f8ff88;
font-weight: bold;
font-size: 110%;
padding: 3px 1em;
@@ -423,7 +424,8 @@ button {
}
.group-avatar {
background-color: aliceblue;
background-color: #f0f8ff88;
color: inherit;
font-weight: bold;
font-size: 90%;
padding: 3px 1em;
@@ -546,6 +548,7 @@ button {
position: relative;
height: 12px;
width: 96%;
margin: auto;
border: 4px solid black;
overflow: hidden;
}

View File

@@ -1,36 +1,45 @@
import { useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import { apiAuth } from "./api";
import { PlayerRanking } from "./types";
import RaceChart from "./RaceChart";
import { useSession } from "./Session";
const MVPChart = () => {
const [data, setData] = useState({} as PlayerRanking[]);
let initialData = {} as PlayerRanking[];
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [showStd, setShowStd] = useState(false);
const { teams } = useSession();
async function loadData() {
setLoading(true);
await apiAuth("analysis/mvp", null)
.then((json) => json as Promise<PlayerRanking[]>)
.then((json) => {
setData(json.sort((a, b) => a.rank - b.rank));
});
setLoading(false);
if (teams) {
await apiAuth(`analysis/mvp/${teams?.activeTeam}`, null)
.then((data) => {
if (data.detail) {
setError(data.detail);
return initialData;
} else {
setError("");
return data as Promise<PlayerRanking[]>;
}
})
.then((data) => {
setData(data.sort((a, b) => a.rank - b.rank));
})
.catch(() => setError("no access"));
setLoading(false);
} else setError("team unknown");
}
useEffect(() => {
loadData();
}, []);
}, [teams]);
return (
<>
{loading ? (
<span className="loader" />
) : (
<RaceChart std={showStd} players={data} />
)}
</>
);
if (loading) return <span className="loader" />;
else if (error) return <span>{error}</span>;
else return <RaceChart std={showStd} players={data} />;
};
export default MVPChart;

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { apiAuth } from "./api";
import {
GraphCanvas,
@@ -10,6 +10,7 @@ import {
useSelection,
} from "reagraph";
import { customTheme } from "./NetworkTheme";
import { useSession } from "./Session";
interface NetworkData {
nodes: GraphNode[];
@@ -36,26 +37,38 @@ const useCustomSelection = (props: CustomSelectionProps): SelectionResult => {
};
export const GraphComponent = () => {
const [data, setData] = useState({ nodes: [], edges: [] } as NetworkData);
let initialData = { nodes: [], edges: [] } as NetworkData;
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [threed, setThreed] = useState(false);
const [likes, setLikes] = useState(2);
const [popularity, setPopularity] = useState(false);
const [mutuality, setMutuality] = useState(false);
const { teams } = useSession();
async function loadData() {
setLoading(true);
await apiAuth("analysis/graph_json", null)
.then((json) => json as Promise<NetworkData>)
.then((json) => {
setData(json);
});
setLoading(false);
if (teams) {
await apiAuth(`analysis/graph_json/${teams?.activeTeam}`, null)
.then((data) => {
if (data.detail) {
setError(data.detail);
return initialData;
} else {
setError("");
return data as Promise<NetworkData>;
}
})
.then((data) => setData(data))
.catch(() => setError("no access"));
setLoading(false);
} else setError("team unknown");
}
useEffect(() => {
loadData();
}, []);
}, [teams]);
const graphRef = useRef<GraphCanvasRef | null>(null);
@@ -161,6 +174,74 @@ export const GraphComponent = () => {
type: "multiModifier",
});
let content: ReactNode;
if (loading) {
content = <span className="loader" />;
} else if (error) {
content = <span>{error}</span>;
} else {
content = (
<>
<GraphCanvas
draggable
cameraMode={threed ? "rotate" : "pan"}
layoutType={threed ? "forceDirected3d" : "forceDirected2d"}
layoutOverrides={{
nodeStrength: -200,
linkDistance: 100,
}}
labelType="nodes"
sizingType="attribute"
sizingAttribute={popularity ? "inDegree" : undefined}
ref={graphRef}
theme={customTheme}
nodes={data.nodes}
edges={data.edges.filter(
(edge) => edge.data.relation === likes || likes === 1
)}
selections={selections}
actives={actives}
onCanvasClick={onCanvasClick}
onNodeClick={onNodeClick}
onNodePointerOut={onNodePointerOut}
onNodePointerOver={onNodePointerOver}
/>
<button
className="infobutton"
onClick={() => {
const dialog = document.querySelector("dialog[id='InfoDialog']");
(dialog as HTMLDialogElement).showModal();
}}
>
info
</button>
<dialog
id="InfoDialog"
style={{ textAlign: "left" }}
onClick={(event) => {
event.currentTarget.close();
}}
>
scroll to zoom
<br />
<br />
<b>hover</b>: show inbound links
<br />
<b>click</b>: show outward links
<br />
<br />
multi-selection possible
<br />
with <i>Ctrl</i> or <i>Shift</i>
<br />
<br />
drag to pan/rotate
</dialog>
</>
);
}
return (
<div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: 0 }}>
<div className="controls">
@@ -225,67 +306,7 @@ export const GraphComponent = () => {
</span>
</div>
)}
{loading ? (
<span className="loader" />
) : (
<GraphCanvas
draggable
cameraMode={threed ? "rotate" : "pan"}
layoutType={threed ? "forceDirected3d" : "forceDirected2d"}
layoutOverrides={{
nodeStrength: -200,
linkDistance: 100,
}}
labelType="nodes"
sizingType="attribute"
sizingAttribute={popularity ? "inDegree" : undefined}
ref={graphRef}
theme={customTheme}
nodes={data.nodes}
edges={data.edges.filter(
(edge) => edge.data.relation === likes || likes === 1
)}
selections={selections}
actives={actives}
onCanvasClick={onCanvasClick}
onNodeClick={onNodeClick}
onNodePointerOut={onNodePointerOut}
onNodePointerOver={onNodePointerOver}
/>
)}
<button
className="infobutton"
onClick={() => {
const dialog = document.querySelector("dialog[id='InfoDialog']");
(dialog as HTMLDialogElement).showModal();
}}
>
info
</button>
<dialog
id="InfoDialog"
style={{ textAlign: "left" }}
onClick={(event) => {
event.currentTarget.close();
}}
>
scroll to zoom
<br />
<br />
<b>hover</b>: show inbound links
<br />
<b>click</b>: show outward links
<br />
<br />
multi-selection possible
<br />
with <i>Ctrl</i> or <i>Shift</i>
<br />
<br />
drag to pan/rotate
</dialog>
{content}
</div>
);
};

View File

@@ -8,7 +8,7 @@ import {
} from "react";
import { ReactSortable, ReactSortableProps } from "react-sortablejs";
import { apiAuth, User } from "./api";
import { useSession } from "./Session";
import { TeamState, useSession } from "./Session";
import { Chemistry, MVPRanking } from "./types";
import TabController from "./TabController";
@@ -56,10 +56,11 @@ function filterSort(list: User[], ids: number[]): User[] {
interface PlayerInfoProps {
user: User;
teams: TeamState;
players: User[];
}
function ChemistryDnD({ user, players }: PlayerInfoProps) {
function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
var otherPlayers = players.filter((player) => player.id !== user.id);
const [playersLeft, setPlayersLeft] = useState<User[]>([]);
const [playersMiddle, setPlayersMiddle] = useState<User[]>(otherPlayers);
@@ -68,6 +69,11 @@ function ChemistryDnD({ user, players }: PlayerInfoProps) {
useEffect(() => {
setPlayersMiddle(otherPlayers);
}, [players]);
useEffect(() => {
setPlayersLeft([]);
setPlayersMiddle(otherPlayers);
setPlayersRight([]);
}, [teams]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
@@ -78,7 +84,13 @@ function ChemistryDnD({ user, players }: PlayerInfoProps) {
let left = playersLeft.map(({ id }) => id);
let middle = playersMiddle.map(({ id }) => id);
let right = playersRight.map(({ id }) => id);
const data = { user: user.id, hate: left, undecided: middle, love: right };
const data = {
user: user.id,
hate: left,
undecided: middle,
love: right,
team: teams.activeTeam,
};
const response = await apiAuth("chemistry", data, "PUT");
setDialog(response || "try sending again");
}
@@ -163,7 +175,7 @@ function ChemistryDnD({ user, players }: PlayerInfoProps) {
);
}
function MVPDnD({ user, players }: PlayerInfoProps) {
function MVPDnD({ user, teams, players }: PlayerInfoProps) {
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
const [rankedPlayers, setRankedPlayers] = useState<User[]>([]);
@@ -171,6 +183,11 @@ function MVPDnD({ user, players }: PlayerInfoProps) {
setAvailablePlayers(players);
}, [players]);
useEffect(() => {
setAvailablePlayers(players);
setRankedPlayers([]);
}, [teams]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
@@ -178,7 +195,7 @@ function MVPDnD({ user, players }: PlayerInfoProps) {
if (dialogRef.current) dialogRef.current.showModal();
setDialog("sending...");
let mvps = rankedPlayers.map(({ id }) => id);
const data = { user: user.id, mvps: mvps };
const data = { user: user.id, mvps: mvps, team: teams.activeTeam };
const response = await apiAuth("mvps", data, "PUT");
response ? setDialog(response) : setDialog("try sending again");
}
@@ -284,7 +301,7 @@ export default function Rankings() {
if (teams) {
try {
const data = await apiAuth(
`player/list?team_id=${teams?.activeTeam}`,
`player/list/${teams?.activeTeam}`,
null,
"GET"
);
@@ -308,8 +325,8 @@ export default function Rankings() {
<>
{user && teams && players ? (
<TabController tabs={tabs}>
<ChemistryDnD {...{ user, players }} />
<MVPDnD {...{ user, players }} />
<ChemistryDnD {...{ user, teams, players }} />
<MVPDnD {...{ user, teams, players }} />
</TabController>
) : (
<span className="loader" />

View File

@@ -62,7 +62,7 @@ export function SessionProvider(props: SessionProviderProps) {
useEffect(() => {
loadUser();
setTimeout(() => loadTeam(), 500);
loadTeam();
}, []);
function onLogin(user: User) {

View File

@@ -1,5 +1,4 @@
import { useSession } from "./Session";
import { Team } from "./types";
export const baseUrl = import.meta.env.VITE_BASE_URL as string;