Compare commits

..

No commits in common. "main" and "feat/demo" have entirely different histories.

20 changed files with 608 additions and 952 deletions

View File

@ -8,15 +8,7 @@ from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlmodel import Session, func, select from sqlmodel import Session, func, select
from sqlmodel.sql.expression import SelectOfScalar from sqlmodel.sql.expression import SelectOfScalar
from cutt.db import ( from cutt.db import Chemistry, MVPRanking, Player, PlayerTeamLink, Team, engine
Chemistry,
MVPRanking,
Player,
PlayerTeamLink,
PlayerType,
Team,
engine,
)
import networkx as nx import networkx as nx
import numpy as np import numpy as np
import matplotlib import matplotlib
@ -33,7 +25,6 @@ analysis_router = APIRouter(prefix="/analysis", tags=["analysis"])
C = Chemistry C = Chemistry
R = MVPRanking R = MVPRanking
PT = PlayerType
P = Player P = Player
@ -153,12 +144,8 @@ def graph_json(
) )
for c in session.exec(statement2): for c in session.exec(statement2):
if c.user not in player_map:
continue
user = player_map[c.user] user = player_map[c.user]
for i, p_id in enumerate(c.love): for i, p_id in enumerate(c.love):
if p_id not in player_map:
continue
p = player_map[p_id] p = player_map[p_id]
edges.append( edges.append(
{ {
@ -174,8 +161,6 @@ def graph_json(
} }
) )
for p_id in c.hate: for p_id in c.hate:
if p_id not in player_map:
continue
p = player_map[p_id] p = player_map[p_id]
edges.append( edges.append(
{ {
@ -308,67 +293,25 @@ async def render_sociogram(params: Params):
return {"image": encoded_image} return {"image": encoded_image}
translate_tablename = { def mvp(
R.__tablename__: "🏆",
C.__tablename__: "🧪",
PT.__tablename__: "🃏",
}
def last_submissions(
request: Annotated[TeamScopedRequest, Security(verify_team_scope)], request: Annotated[TeamScopedRequest, Security(verify_team_scope)],
): ):
times = {} ranks = dict()
with Session(engine) as session:
player_ids = session.exec(
select(P.id)
.join(PlayerTeamLink)
.join(Team)
.where(Team.id == request.team_id, P.disabled == False)
).all()
for survey in [C, PT, R]:
subquery = (
select(survey.user, func.max(survey.time).label("latest"))
.where(survey.team == request.team_id)
.where(survey.user.in_(player_ids))
.group_by(survey.user)
.subquery()
)
statement2 = select(survey).join(
subquery,
(survey.user == subquery.c.user) & (survey.time == subquery.c.latest),
)
for r in session.exec(statement2):
if r.time.date() not in times:
times[r.time.date()] = {}
times[r.time.date()][r.user] = (
times[r.time.date()].get(r.user, "")
+ translate_tablename[survey.__tablename__]
)
return times
def mvp(
request: Annotated[TeamScopedRequest, Security(verify_team_scope)], mixed=False
):
if request.team_id == 42: if request.team_id == 42:
ranks = {}
random.seed(42) random.seed(42)
players = [request.user] + demo_players players = [request.user] + demo_players
for p in players: for p in players:
random.shuffle(players) random.shuffle(players)
for i, p in enumerate(players): for i, p in enumerate(players):
ranks[p.id] = ranks.get(p.id, []) + [i + 1] ranks[p.display_name] = ranks.get(p.display_name, []) + [i + 1]
return [ return [
[ {
{ "name": p,
"p_id": p_id, "rank": f"{np.mean(v):.02f}",
"rank": f"{np.mean(v):.02f}", "std": f"{np.std(v):.02f}",
"std": f"{np.std(v):.02f}", "n": len(v),
"n": len(v), }
} for p, v in ranks.items()
for i, (p_id, v) in enumerate(ranks.items())
]
] ]
with Session(engine) as session: with Session(engine) as session:
@ -380,7 +323,7 @@ def mvp(
).all() ).all()
if not players: if not players:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
player_map = {p.id: p for p in players} player_map = {p.id: p.display_name for p in players}
subquery = ( subquery = (
select(R.user, func.max(R.time).label("latest")) select(R.user, func.max(R.time).label("latest"))
.where(R.team == request.team_id) .where(R.team == request.team_id)
@ -390,45 +333,23 @@ def mvp(
statement2 = select(R).join( statement2 = select(R).join(
subquery, (R.user == subquery.c.user) & (R.time == subquery.c.latest) subquery, (R.user == subquery.c.user) & (R.time == subquery.c.latest)
) )
if mixed: for r in session.exec(statement2):
all_ranks = [] for i, p_id in enumerate(r.mvps):
for gender in ["fmp", "mmp"]: p = player_map[p_id]
ranks = {} ranks[p] = ranks.get(p, []) + [i + 1]
for r in session.exec(statement2):
mvps = [
p_id
for p_id in r.mvps
if p_id in player_map and player_map[p_id].gender == gender
]
for i, p_id in enumerate(mvps):
p = player_map[p_id]
ranks[p_id] = ranks.get(p_id, []) + [i + 1]
all_ranks.append(ranks)
else:
ranks = {}
for r in session.exec(statement2):
for i, p_id in enumerate(r.mvps):
if p_id not in player_map:
continue
p = player_map[p_id]
ranks[p_id] = ranks.get(p_id, []) + [i + 1]
all_ranks = [ranks]
if not all_ranks: if not ranks:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="no entries found" status_code=status.HTTP_404_NOT_FOUND, detail="no entries found"
) )
return [ return [
[ {
{ "name": p,
"p_id": p_id, "rank": f"{np.mean(v):.02f}",
"rank": f"{np.mean(v):.02f}", "std": f"{np.std(v):.02f}",
"std": f"{np.std(v):.02f}", "n": len(v),
"n": len(v), }
} for p, v in ranks.items()
for p_id, v in ranks.items()
]
for ranks in all_ranks
] ]
@ -503,9 +424,6 @@ analysis_router.add_api_route(
description="Request Most Valuable Players stats", description="Request Most Valuable Players stats",
) )
analysis_router.add_api_route("/turnout/{team_id}", endpoint=turnout, methods=["GET"]) analysis_router.add_api_route("/turnout/{team_id}", endpoint=turnout, methods=["GET"])
analysis_router.add_api_route(
"/times/{team_id}", endpoint=last_submissions, methods=["GET"]
)
if __name__ == "__main__": if __name__ == "__main__":
with Session(engine) as session: with Session(engine) as session:

View File

@ -1,7 +1,6 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlmodel import ( from sqlmodel import (
ARRAY, ARRAY,
CHAR,
Column, Column,
Integer, Integer,
Relationship, Relationship,
@ -38,7 +37,6 @@ class Team(SQLModel, table=True):
name: str name: str
location: str | None location: str | None
country: str | None country: str | None
mixed: bool = False
players: list["Player"] | None = Relationship( players: list["Player"] | None = Relationship(
back_populates="teams", link_model=PlayerTeamLink back_populates="teams", link_model=PlayerTeamLink
) )
@ -50,7 +48,6 @@ class Player(SQLModel, table=True):
display_name: str display_name: str
email: str | None = None email: str | None = None
full_name: str | None = None full_name: str | None = None
gender: str | None = Field(default=None, sa_column=Column(CHAR(3)))
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
@ -70,16 +67,6 @@ class Chemistry(SQLModel, table=True):
team: int = Field(default=None, foreign_key="team.id") team: int = Field(default=None, foreign_key="team.id")
class PlayerType(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
time: datetime | None = Field(default_factory=utctime)
user: int = Field(default=None, foreign_key="player.id")
handlers: list[int] = Field(sa_column=Column(ARRAY(Integer)))
combis: list[int] = Field(sa_column=Column(ARRAY(Integer)))
cutters: list[int] = Field(sa_column=Column(ARRAY(Integer)))
team: int = Field(default=None, foreign_key="team.id")
class MVPRanking(SQLModel, table=True): class MVPRanking(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
time: datetime | None = Field(default_factory=utctime) time: datetime | None = Field(default_factory=utctime)

View File

@ -2,27 +2,26 @@ import random
from cutt.db import Player from cutt.db import Player
names = [ names = [
("August", "mmp"), "August",
("Beate", "fmp"), "Beate",
("Ceasar", "mmp"), "Ceasar",
("Daedalus", "mmp"), "Daedalus",
("Elli", "fmp"), "Elli",
("Ford P.", ""), "Ford P.",
("Gabriel", "mmp"), "Gabriel",
("Hugo", "mmp"), "Hugo",
("Ivar Johansson", "mmp"), "Ivar Johansson",
("Jürgen Gordon Malinauskas", "mmp"), "Jürgen Gordon Malinauskas",
] ]
demo_players = [ demo_players = [
Player.model_validate( Player.model_validate(
{ {
"id": i + 4200, "id": i,
"display_name": name, "display_name": name,
"username": name.lower().replace(" ", "").replace(".", ""), "username": name.lower().replace(" ", "").replace(".", ""),
"gender": gender,
"number": str(random.randint(0, 100)), "number": str(random.randint(0, 100)),
"email": name.lower().replace(" ", "").replace(".", "") + "@example.org", "email": name.lower().replace(" ", "").replace(".", "") + "@example.org",
} }
) )
for i, (name, gender) in enumerate(names) for i, name in enumerate(names)
] ]

View File

@ -2,7 +2,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from cutt.db import Player, PlayerType, Team, Chemistry, MVPRanking, engine from cutt.db import Player, Team, Chemistry, MVPRanking, engine
from sqlmodel import ( from sqlmodel import (
Session, Session,
func, func,
@ -20,7 +20,6 @@ from cutt.security import (
from cutt.player import player_router from cutt.player import player_router
C = Chemistry C = Chemistry
PT = PlayerType
R = MVPRanking R = MVPRanking
P = Player P = Player
@ -115,7 +114,7 @@ def get_mvps(
return mvps return mvps
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_404_NOT_FOUND,
detail="no previous state was found", detail="no previous state was found",
) )
@ -159,61 +158,11 @@ def get_chemistry(
subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest) subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest)
) )
chemistry = session.exec(statement2).one_or_none() chemistry = session.exec(statement2).one_or_none()
if chemistry is not None: if chemistry:
return chemistry return chemistry
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_404_NOT_FOUND,
detail="no previous state was found",
)
@api_router.put("/playertype", tags=["analysis"])
def submit_playertype(
playertype: PlayerType, user: Annotated[Player, Depends(get_current_active_user)]
):
if playertype.team == 42:
return JSONResponse("DEMO team, nothing happens")
if user.id == playertype.user:
with Session(engine) as session:
statement = select(Team).where(Team.id == playertype.team)
players = [t.players for t in session.exec(statement)][0]
if players:
player_ids = {p.id for p in players}
if player_ids >= (
set(playertype.handlers)
| set(playertype.combis)
| set(playertype.cutters)
):
session.add(playertype)
session.commit()
return JSONResponse("success!")
raise somethings_fishy
else:
raise wrong_user_id_exception
@api_router.get("/playertype/{team_id}", tags=["analysis"])
def get_playertype(
team_id: int, user: Annotated[Player, Depends(get_current_active_user)]
):
with Session(engine) as session:
subquery = (
select(PT.user, func.max(PT.time).label("latest"))
.where(PT.user == user.id)
.where(PT.team == team_id)
.group_by(PT.user)
.subquery()
)
statement2 = select(PT).join(
subquery, (PT.user == subquery.c.user) & (PT.time == subquery.c.latest)
)
playertype = session.exec(statement2).one_or_none()
if playertype is not None:
return playertype
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="no previous state was found", detail="no previous state was found",
) )

View File

@ -22,9 +22,8 @@ player_router = APIRouter(prefix="/player", tags=["player"])
class PlayerRequest(BaseModel): class PlayerRequest(BaseModel):
display_name: str display_name: str
username: str username: str
gender: str | None
number: str number: str
email: str | None email: str
class AddPlayerRequest(PlayerRequest): ... class AddPlayerRequest(PlayerRequest): ...
@ -62,7 +61,6 @@ def add_player(
new_player = Player( new_player = Player(
username=r.username, username=r.username,
display_name=r.display_name, display_name=r.display_name,
gender=r.gender if r.gender else None,
email=r.email if r.email else None, email=r.email if r.email else None,
number=r.number, number=r.number,
disabled=False, disabled=False,
@ -91,11 +89,9 @@ def modify_player(
.where(Team.id == request.team_id, P.id == r.id, P.username == r.username) .where(Team.id == request.team_id, P.id == r.id, P.username == r.username)
).one_or_none() ).one_or_none()
if player: if player:
print(r)
player.display_name = r.display_name.strip() player.display_name = r.display_name.strip()
player.number = r.number.strip() player.number = r.number.strip()
player.gender = r.gender.strip() if r.gender else None player.email = r.email.strip()
player.email = r.email.strip() if r.email else None
session.add(player) session.add(player)
session.commit() session.commit()
return PlainTextResponse("modification successful") return PlainTextResponse("modification successful")
@ -166,7 +162,7 @@ async def list_players(
if team_id == 42: if team_id == 42:
return [ return [
user.model_dump( user.model_dump(
include={"id", "display_name", "gender", "username", "number", "email"} include={"id", "display_name", "username", "number", "email"}
) )
] + demo_players ] + demo_players
@ -188,19 +184,11 @@ async def list_players(
.join(PlayerTeamLink) .join(PlayerTeamLink)
.join(Team) .join(Team)
.where(Team.id == team_id, P.disabled == False) .where(Team.id == team_id, P.disabled == False)
.order_by(P.display_name)
).all() ).all()
if players: if players:
return [ return [
player.model_dump( player.model_dump(
include={ include={"id", "display_name", "username", "number", "email"}
"id",
"display_name",
"username",
"gender",
"number",
"email",
}
) )
for player in players for player in players
if not player.disabled if not player.disabled

228
src/Analysis.tsx Normal file
View File

@ -0,0 +1,228 @@
import { useEffect, useState } from "react";
import { apiAuth } from "./api";
//const debounce = <T extends (...args: any[]) => void>(
// func: T,
// delay: number
//): ((...args: Parameters<T>) => void) => {
// let timeoutId: number | null = null;
// return (...args: Parameters<T>) => {
// if (timeoutId !== null) {
// clearTimeout(timeoutId);
// }
// console.log(timeoutId);
// timeoutId = setTimeout(() => {
// func(...args);
// }, delay);
// };
//};
//
interface Params {
nodeSize: number;
edgeWidth: number;
arrowSize: number;
fontSize: number;
distance: number;
weighting: boolean;
popularity: boolean;
show: number;
}
let timeoutID: NodeJS.Timeout | null = null;
export default function Analysis() {
const [image, setImage] = useState("");
const [params, setParams] = useState<Params>({
nodeSize: 2000,
edgeWidth: 1,
arrowSize: 16,
fontSize: 10,
distance: 2,
weighting: true,
popularity: true,
show: 2,
});
const [showControlPanel, setShowControlPanel] = useState(false);
const [loading, setLoading] = useState(true);
// Function to generate and fetch the graph image
async function loadImage() {
setLoading(true);
await apiAuth("analysis/image", params, "POST")
.then((data) => {
setImage(data.image);
setLoading(false);
})
.catch((e) => {
console.log("best to just reload... ", e);
});
}
useEffect(() => {
if (timeoutID) {
clearTimeout(timeoutID);
}
timeoutID = setTimeout(() => {
loadImage();
}, 1000);
}, [params]);
function showLabel() {
switch (params.show) {
case 0:
return "dislike";
case 1:
return "both";
case 2:
return "like";
}
}
return (
<div className="stack column dropdown">
<button onClick={() => setShowControlPanel(!showControlPanel)}>
Parameters{" "}
<svg
viewBox="0 0 24 24"
height="1.2em"
style={{
fill: "#ffffff",
display: "inline",
top: "0.2em",
position: "relative",
transform: showControlPanel ? "rotate(180deg)" : "unset",
}}
>
{" "}
<path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z"> </path>
</svg>
</button>
<div id="control-panel" className={showControlPanel ? "opened" : ""}>
<div className="control">
<datalist id="markers">
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
</datalist>
<div id="three-slider">
<label>😬</label>
<input
type="range"
list="markers"
min="0"
max="2"
step="1"
width="16px"
onChange={(evt) =>
setParams({ ...params, show: Number(evt.target.value) })
}
/>
<label>😍</label>
</div>
{showLabel()}
</div>
<div className="control">
<div className="checkBox">
<input
type="checkbox"
checked={params.weighting}
onChange={(evt) =>
setParams({ ...params, weighting: evt.target.checked })
}
/>
<label>weighting</label>
</div>
<div className="checkBox">
<input
type="checkbox"
checked={params.popularity}
onChange={(evt) =>
setParams({ ...params, popularity: evt.target.checked })
}
/>
<label>popularity</label>
</div>
</div>
<div className="control">
<label>distance between nodes</label>
<input
type="range"
min="0.01"
max="3.001"
step="0.05"
value={params.distance}
onChange={(evt) =>
setParams({ ...params, distance: Number(evt.target.value) })
}
/>
<span>{params.distance}</span>
</div>
<div className="control">
<label>node size</label>
<input
type="range"
min="500"
max="3000"
value={params.nodeSize}
onChange={(evt) =>
setParams({ ...params, nodeSize: Number(evt.target.value) })
}
/>
<span>{params.nodeSize}</span>
</div>
<div className="control">
<label>font size</label>
<input
type="range"
min="4"
max="24"
value={params.fontSize}
onChange={(evt) =>
setParams({ ...params, fontSize: Number(evt.target.value) })
}
/>
<span>{params.fontSize}</span>
</div>
<div className="control">
<label>edge width</label>
<input
type="range"
min="1"
max="5"
step="0.1"
value={params.edgeWidth}
onChange={(evt) =>
setParams({ ...params, edgeWidth: Number(evt.target.value) })
}
/>
<span>{params.edgeWidth}</span>
</div>
<div className="control">
<label>arrow size</label>
<input
type="range"
min="10"
max="50"
value={params.arrowSize}
onChange={(evt) =>
setParams({ ...params, arrowSize: Number(evt.target.value) })
}
/>
<span>{params.arrowSize}</span>
</div>
</div>
<button onClick={() => loadImage()}>reload </button>
{loading ? (
<span className="loader"></span>
) : (
<img src={"data:image/png;base64," + image} width="86%" />
)}
</div>
);
}

View File

@ -29,6 +29,7 @@ dialog {
border-radius: 1em; border-radius: 1em;
} }
/*=========Network Controls=========*/ /*=========Network Controls=========*/
.infobutton { .infobutton {
@ -60,7 +61,7 @@ dialog {
flex-wrap: wrap; flex-wrap: wrap;
max-width: 240px; max-width: 240px;
margin: 0px; margin: 0px;
background-color: #f0f8ffdd; background-color: #F0F8FFdd;
.slider, .slider,
span { span {
@ -102,8 +103,8 @@ dialog {
bottom: 0; bottom: 0;
background-color: #ccc; background-color: #ccc;
border-radius: 34px; border-radius: 34px;
-webkit-transition: 0.4s; -webkit-transition: .4s;
transition: 0.4s; transition: .4s;
} }
.slider:before { .slider:before {
@ -115,19 +116,19 @@ dialog {
bottom: 3px; bottom: 3px;
background-color: white; background-color: white;
border-radius: 50%; border-radius: 50%;
-webkit-transition: 0.4s; -webkit-transition: .4s;
transition: 0.4s; transition: .4s;
} }
input:checked + .slider { input:checked+.slider {
background-color: #2196f3; background-color: #2196F3;
} }
input:focus + .slider { input:focus+.slider {
box-shadow: 0 0 1px #2196f3; box-shadow: 0 0 1px #2196F3;
} }
input:checked + .slider:before { input:checked+.slider:before {
-webkit-transform: translateX(24px); -webkit-transform: translateX(24px);
-ms-transform: translateX(24px); -ms-transform: translateX(24px);
transform: translateX(24px); transform: translateX(24px);
@ -137,6 +138,8 @@ input:checked + .slider:before {
opacity: 66%; opacity: 66%;
} }
.hint { .hint {
position: absolute; position: absolute;
font-size: 80%; font-size: 80%;
@ -148,14 +151,11 @@ input:checked + .slider:before {
z-index: -1; z-index: -1;
} }
input, input {
select {
padding: 0.2em 16px; padding: 0.2em 16px;
margin-top: 0.25em; margin-top: 0.25em;
margin-bottom: 0.25em; margin-bottom: 0.25em;
border-radius: 1em; border-radius: 1em;
color: black;
background-color: white;
} }
h1, h1,
@ -180,6 +180,7 @@ h3 {
flex-direction: column; flex-direction: column;
} }
.container { .container {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -200,9 +201,6 @@ h3 {
border-style: solid; border-style: solid;
border-radius: 16px; border-radius: 16px;
h4 {
margin: 4px;
}
&.one { &.one {
max-width: min(96%, 768px); max-width: min(96%, 768px);
margin: 4px auto; margin: 4px auto;
@ -213,7 +211,6 @@ h3 {
} }
.reservoir { .reservoir {
display: flex;
flex-direction: unset; flex-direction: unset;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-around; justify-content: space-around;
@ -284,8 +281,10 @@ button {
font-size: 80%; font-size: 80%;
margin: 0px; margin: 0px;
} }
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
#control-panel { #control-panel {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -309,6 +308,7 @@ button {
font-size: xx-large; font-size: xx-large;
margin-bottom: 16px; margin-bottom: 16px;
margin-right: 16px; margin-right: 16px;
} }
.wavering { .wavering {
@ -317,13 +317,11 @@ button {
} }
::backdrop { ::backdrop {
background-image: linear-gradient( background-image: linear-gradient(45deg,
45deg, magenta,
magenta, rebeccapurple,
rebeccapurple, dodgerblue,
dodgerblue, green);
green
);
opacity: 0.75; opacity: 0.75;
} }
@ -490,9 +488,7 @@ button {
input { input {
max-width: 300px; max-width: 300px;
} margin: 0.2em auto;
select {
max-width: 335px;
} }
} }
@ -500,7 +496,7 @@ button {
color: black; color: black;
background-color: #36c4; background-color: #36c4;
border: 1px solid black; border: 1px solid black;
border-radius: 1.5em; border-radius: 1.4em;
margin: 4px; margin: 4px;
padding: 0.2em 0.5em; padding: 0.2em 0.5em;
@ -537,27 +533,21 @@ button {
margin: auto 1em; margin: auto 1em;
} }
input, input {
select {
width: 90%; width: 90%;
margin: 4px 0; margin: 4px 0;
} }
} }
} }
.mmp {
background-color: lightskyblue;
}
.fmp {
background-color: salmon;
}
@keyframes blink { @keyframes blink {
0% { 0% {
background-color: #8888; background-color: #8888;
} }
13% { 13% {
background-color: #8888; background-color: #8888;
} }
@ -570,6 +560,7 @@ button {
background-color: #8888; background-color: #8888;
} }
38% { 38% {
background-color: #8888; background-color: #8888;
} }
@ -647,88 +638,3 @@ button {
transform: translateX(0%); transform: translateX(0%);
} }
} }
.calendar-container {
position: relative;
margin: 20px auto;
font-size: small;
}
.month-navigation {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
border-top: 2px solid grey;
border-bottom: 2px solid grey;
}
.month-navigation button {
cursor: pointer;
padding: 4px 8px;
border: none;
color: black;
background-color: transparent;
}
.month-navigation span {
font-weight: bold;
}
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.day {
padding: 2px;
border: 1px solid grey;
cursor: pointer;
display: flex;
}
.selected-day {
border: 4px solid grey;
}
.weekday {
border-bottom: 3px solid black;
margin: 0 1em;
}
.day-circle {
text-align: center;
border-radius: 1.5em;
width: 1.5em;
height: 1.5em;
padding: 0;
margin: auto;
border: 2px solid transparent;
}
.today {
border-radius: 1.6em;
border: 4px solid red;
text-align: center;
}
.has-event {
border-radius: 1.5em;
background-color: lightskyblue;
}
.active-player {
border-radius: 1.5em;
border: 4px solid rebeccapurple;
}
.events {
font-size: large;
padding: 20px;
ul > li {
padding: 0;
margin: 0;
list-style-type: none;
}
}

View File

@ -1,3 +1,4 @@
import Analysis from "./Analysis";
import "./App.css"; import "./App.css";
import Footer from "./Footer"; import Footer from "./Footer";
import Header from "./Header"; import Header from "./Header";
@ -34,6 +35,7 @@ function App() {
<Routes> <Routes>
<Route index element={<Rankings />} /> <Route index element={<Rankings />} />
<Route path="network" element={<GraphComponent />} /> <Route path="network" element={<GraphComponent />} />
<Route path="analysis" element={<Analysis />} />
<Route path="mvp" element={<MVPChart />} /> <Route path="mvp" element={<MVPChart />} />
<Route path="changepassword" element={<SetPassword />} /> <Route path="changepassword" element={<SetPassword />} />
<Route path="team" element={<TeamPanel />} /> <Route path="team" element={<TeamPanel />} />

View File

@ -22,10 +22,6 @@ const UserInfo = (user: User, teams: TeamState | undefined) => {
<b>display name: </b> <b>display name: </b>
</div> </div>
<div>{user?.display_name}</div> <div>{user?.display_name}</div>
<div>
<b>gender: </b>
</div>
<div>{user?.gender?.toUpperCase() || "-"}</div>
<div> <div>
<b>number: </b> <b>number: </b>
</div> </div>

95
src/BarChart.tsx Normal file
View File

@ -0,0 +1,95 @@
import { FC } from 'react';
import { PlayerRanking } from './types';
interface BarChartProps {
players: PlayerRanking[];
width: number;
height: number;
std: boolean;
}
const BarChart: FC<BarChartProps> = ({ players, width, height, std }) => {
const padding = 24;
const maxValue = Math.max(...players.map((player) => player.rank)) + 1;
const barWidth = (width - 2 * padding) / players.length;
return (
<svg width={width} height={height}>
{players.map((player, index) => (
<rect
key={index}
x={index * barWidth + padding}
y={height - (1 - player.rank / maxValue) * height}
width={barWidth - 8} // subtract 2 for some spacing between bars
height={(1 - player.rank / maxValue) * height}
fill="#69f"
/>
))}
{players.map((player, index) => (
<text
key={index}
x={index * barWidth + barWidth / 2 - 4 + padding}
y={height - (1 - player.rank / maxValue) * height - 5}
textAnchor="middle"
//transform='rotate(-27)'
//style={{ transformOrigin: "center", transformBox: "fill-box" }}
fontSize="16px"
fill="#404040"
>
{player.name}
</text>
))}
{players.map((player, index) => (
<text
key={index}
x={index * barWidth + barWidth / 2 + padding - 4}
y={height - 8}
textAnchor="middle"
fontSize="12px"
fill="#404040"
>
{player.rank}
</text>
))}
{std && players.map((player, index) => (
<line
key={`error-${index}`}
x1={index * barWidth + barWidth / 2 + padding}
y1={height - (1 - player.rank / maxValue) * height - (player.std / maxValue) * height}
x2={index * barWidth + barWidth / 2 + padding}
y2={height - (1 - player.rank / maxValue) * height + (player.std / maxValue) * height}
stroke="#ff0000"
strokeWidth="1"
/>
))}
{std && players.map((player, index) => (
<line
key={`cap-${index}-top`}
x1={index * barWidth + barWidth / 2 - 2 + padding}
y1={height - (1 - player.rank / maxValue) * height - (player.std / maxValue) * height}
x2={index * barWidth + barWidth / 2 + 2 + padding}
y2={height - (1 - player.rank / maxValue) * height - (player.std / maxValue) * height}
stroke="#ff0000"
strokeWidth="1"
/>
))}
{std && players.map((player, index) => (
<line
key={`cap-${index}-bottom`}
x1={index * barWidth + barWidth / 2 - 2 + padding}
y1={height - (1 - player.rank / maxValue) * height + (player.std / maxValue) * height}
x2={index * barWidth + barWidth / 2 + 2 + padding}
y2={height - (1 - player.rank / maxValue) * height + (player.std / maxValue) * height}
stroke="#ff0000"
strokeWidth="1"
/>
))}
</svg>
);
};
export default BarChart;

View File

@ -1,174 +0,0 @@
import { useEffect, useState } from "react";
import { apiAuth } from "./api";
import { useSession } from "./Session";
interface Datum {
[id: number]: string;
}
interface Events {
[key: string]: Datum;
}
const Calendar = ({ playerId }: { playerId: number }) => {
const [selectedDate, setSelectedDate] = useState(new Date());
const [events, setEvents] = useState<Events>();
const { teams, players } = useSession();
async function loadSubmissionDates() {
if (teams?.activeTeam) {
const data = await apiAuth(`analysis/times/${teams?.activeTeam}`, null);
if (data.detail) {
console.log(data.detail);
} else {
setEvents(data as Events);
}
}
}
useEffect(() => {
loadSubmissionDates();
}, [players]);
const getEventsForDay = (date: Date) => {
return events && events[date.toISOString().split("T")[0]];
};
// Handle day click
const handleDayClick = (date: Date) => {
setSelectedDate(date);
};
// Navigate to previous month
const handlePrevMonth = () => {
const date = new Date(selectedDate);
date.setMonth(date.getMonth() - 1);
setSelectedDate(date);
};
// Navigate to next month
const handleNextMonth = () => {
const date = new Date(selectedDate);
date.setMonth(date.getMonth() + 1);
setSelectedDate(date);
};
// Render month navigation
const renderMonthNavigation = () => {
return (
<div className="month-navigation">
<button onClick={handlePrevMonth}>&lt;</button>
<span>
<button onClick={() => setSelectedDate(new Date())}>📅</button>
{selectedDate.toLocaleString("default", {
month: "long",
year: "numeric",
})}
</span>
<button onClick={handleNextMonth}>&gt;</button>
</div>
);
};
// Render the calendar
const renderCalendar = () => {
const firstDayOfMonth = new Date(
selectedDate.getFullYear(),
selectedDate.getMonth(),
0
).getDay();
const lastDateOfMonth = new Date(
selectedDate.getFullYear(),
selectedDate.getMonth() + 1,
0
).getDate();
let days: JSX.Element[] = [];
let day = 1;
for (let i = 0; i < 7; i++) {
const date = new Date(0);
date.setDate(i + 5);
days.push(
<div key={"weekday_" + i} className="weekday">
{date.toLocaleString("default", {
weekday: "narrow",
})}
</div>
);
}
// Add empty cells for the first week
for (let i = 0; i < firstDayOfMonth; i++) {
days.push(<div key={"prev" + i} className="empty"></div>);
}
// Render each day of the month
while (day <= lastDateOfMonth) {
const date = new Date(selectedDate);
date.setDate(day);
const todaysEvents = getEventsForDay(date);
days.push(
<div
key={date.getDate()}
className={
"day" +
(date.toDateString() === selectedDate.toDateString()
? " selected-day"
: "")
}
onClick={() => handleDayClick(date)}
>
<div
className={
"day-circle" +
(date.toDateString() === new Date().toDateString()
? " today"
: "") +
(todaysEvents ? " has-event" : "") +
(todaysEvents && playerId in todaysEvents ? " active-player" : "")
}
>
{day}
</div>
</div>
);
day++;
}
return <div className="calendar">{days}</div>;
};
// Render events for the selected day
const renderEvents = () => {
const eventsForDay = getEventsForDay(selectedDate);
return (
<div className="events">
{eventsForDay && (
<ul>
{Object.entries(eventsForDay).map(([id, sub]) => {
const name = players?.find((p) => p.id === Number(id));
return (
<li key={id}>
{name !== undefined ? name.display_name : ""}:{" "}
<span style={{ letterSpacing: 8 }}>{sub}</span>
</li>
);
})}
</ul>
)}
</div>
);
};
return (
<div className="calendar-container">
<h2>Latest Submissions</h2>
{renderMonthNavigation()}
{renderCalendar()}
{renderEvents()}
</div>
);
};
export default Calendar;

View File

@ -15,7 +15,7 @@ export default function Footer() {
</Link> </Link>
<span>|</span> <span>|</span>
<Link to="/network"> <Link to="/network">
<span>Sociogram</span> <span>Trainer Analysis</span>
</Link> </Link>
<span>|</span> <span>|</span>
<Link to="/mvp"> <Link to="/mvp">

View File

@ -6,13 +6,12 @@ import { useSession } from "./Session";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
const MVPChart = () => { const MVPChart = () => {
let initialData = {} as PlayerRanking[][]; let initialData = {} as PlayerRanking[];
const [data, setData] = useState(initialData); const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [showStd, setShowStd] = useState(false); const [showStd, setShowStd] = useState(false);
const { user, teams } = useSession(); const { user, teams } = useSession();
const [mixed, setMixed] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) || user?.scopes.includes(`team:${teams?.activeTeam}`) ||
@ -20,30 +19,21 @@ const MVPChart = () => {
navigate("/", { replace: true }); navigate("/", { replace: true });
}, [user]); }, [user]);
useEffect(() => {
if (teams) {
const activeTeam = teams.teams.find(
(team) => team.id == teams.activeTeam
);
activeTeam && setMixed(activeTeam.mixed);
}
}, [teams]);
async function loadData() { async function loadData() {
setLoading(true); setLoading(true);
if (teams) { if (teams) {
await apiAuth(`analysis/mvp/${teams?.activeTeam}?mixed=${mixed}`, null) await apiAuth(`analysis/mvp/${teams?.activeTeam}`, null)
.then((data) => { .then((data) => {
if (data.detail) { if (data.detail) {
setError(data.detail); setError(data.detail);
return initialData; return initialData;
} else { } else {
setError(""); setError("");
return data as Promise<PlayerRanking[][]>; return data as Promise<PlayerRanking[]>;
} }
}) })
.then((data) => { .then((data) => {
setData(data.map((_data) => _data.sort((a, b) => a.rank - b.rank))); setData(data.sort((a, b) => a.rank - b.rank));
}) })
.catch(() => setError("no access")); .catch(() => setError("no access"));
setLoading(false); setLoading(false);
@ -56,8 +46,7 @@ const MVPChart = () => {
if (loading) return <span className="loader" />; if (loading) return <span className="loader" />;
else if (error) return <span>{error}</span>; else if (error) return <span>{error}</span>;
else else return <RaceChart std={showStd} players={data} />;
return data.map((_data) => <RaceChart std={showStd} playerRanks={_data} />);
}; };
export default MVPChart; export default MVPChart;

View File

@ -1,9 +1,8 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { PlayerRanking } from "./types"; import { PlayerRanking } from "./types";
import { useSession } from "./Session";
interface RaceChartProps { interface RaceChartProps {
playerRanks: PlayerRanking[]; players: PlayerRanking[];
std: boolean; std: boolean;
} }
@ -14,14 +13,15 @@ const determineNiceWidth = (width: number) => {
else return width * 0.96; else return width * 0.96;
}; };
const RaceChart: FC<RaceChartProps> = ({ playerRanks, std }) => { const RaceChart: FC<RaceChartProps> = ({ players, std }) => {
const [width, setWidth] = useState(determineNiceWidth(window.innerWidth)); const [width, setWidth] = useState(determineNiceWidth(window.innerWidth));
const height = (playerRanks.length + 1) * 40; //const [height, setHeight] = useState(window.innerHeight);
const { players } = useSession(); const height = (players.length + 1) * 40;
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
setWidth(determineNiceWidth(window.innerWidth)); setWidth(determineNiceWidth(window.innerWidth));
//setHeight(window.innerHeight);
}; };
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
return () => { return () => {
@ -30,18 +30,18 @@ const RaceChart: FC<RaceChartProps> = ({ playerRanks, std }) => {
}, []); }, []);
const padding = 24; const padding = 24;
const gap = 8; const gap = 8;
const maxValue = Math.max(...playerRanks.map((player) => player.rank)) + 1; const maxValue = Math.max(...players.map((player) => player.rank)) + 1;
const barHeight = (height - 2 * padding) / playerRanks.length; const barHeight = (height - 2 * padding) / players.length;
const fontSize = Math.min(barHeight - 1.5 * gap, width / 22); const fontSize = Math.min(barHeight - 1.5 * gap, width / 22);
return ( return (
<svg width={width} height={height} id="RaceChartSVG"> <svg width={width} height={height} id="RaceChartSVG">
{playerRanks.map((playerRank, index) => ( {players.map((player, index) => (
<rect <rect
key={String(index)} key={String(index)}
x={4} x={4}
y={index * barHeight + padding} y={index * barHeight + padding}
width={(1 - playerRank.rank / maxValue) * width} width={(1 - player.rank / maxValue) * width}
height={barHeight - gap} // subtract 2 for some spacing between bars height={barHeight - gap} // subtract 2 for some spacing between bars
fill="#36c" fill="#36c"
stroke="aliceblue" stroke="aliceblue"
@ -50,53 +50,49 @@ const RaceChart: FC<RaceChartProps> = ({ playerRanks, std }) => {
/> />
))} ))}
{playerRanks.map((playerRank, index) => { {players.map((player, index) => (
const player = players!.find((p) => p.id === playerRank.p_id); <g key={"group" + index}>
return ( <text
<g key={"group" + index}> key={index + "_name"}
<text x={8}
key={index + "_name"} y={index * barHeight + barHeight / 2 + padding + gap / 2}
x={8} width={(1 - player.rank / maxValue) * width}
y={index * barHeight + barHeight / 2 + padding + gap / 2} height={barHeight - 8} // subtract 2 for some spacing between bars
width={(1 - playerRank.rank / maxValue) * width} fontSize={fontSize}
height={barHeight - 8} // subtract 2 for some spacing between bars fill="aliceblue"
fontSize={fontSize} stroke="#36c"
fill="aliceblue" strokeWidth={4}
stroke="#36c" fontWeight={"bold"}
strokeWidth={4} paintOrder={"stroke fill"}
fontWeight={"bold"} fontFamily="monospace"
paintOrder={"stroke fill"} style={{ whiteSpace: "pre" }}
fontFamily="monospace" >
style={{ whiteSpace: "pre" }} {`${String(index + 1).padStart(2)}. ${player.name}`}
> </text>
{`${String(index + 1).padStart(2)}. ${player?.display_name}`} <text
</text> key={index + "_value"}
<text x={
key={index + "_value"} 8 +
x={ (4 + Math.max(...players.map((p, _) => p.name.length))) *
8 + fontSize *
(4 + 0.66
Math.max(...players!.map((p, _) => p.display_name.length))) * }
fontSize * y={index * barHeight + barHeight / 2 + padding + gap / 2}
0.66 width={(1 - player.rank / maxValue) * width}
} height={barHeight - 8} // subtract 2 for some spacing between bars
y={index * barHeight + barHeight / 2 + padding + gap / 2} fontSize={0.8 * fontSize}
width={(1 - playerRank.rank / maxValue) * width} fill="aliceblue"
height={barHeight - 8} // subtract 2 for some spacing between bars stroke="#36c"
fontSize={0.8 * fontSize} fontWeight={"bold"}
fill="aliceblue" fontFamily="monospace"
stroke="#36c" strokeWidth={4}
fontWeight={"bold"} paintOrder={"stroke fill"}
fontFamily="monospace" style={{ whiteSpace: "pre" }}
strokeWidth={4} >
paintOrder={"stroke fill"} {`${String(player.rank).padStart(5)} ± ${player.std} N = ${player.n}`}
style={{ whiteSpace: "pre" }} </text>
> </g>
{`${String(playerRank.rank).padStart(5)} ± ${playerRank.std} N = ${playerRank.n}`} ))}
</text>
</g>
);
})}
</svg> </svg>
); );
}; };

View File

@ -1,41 +1,31 @@
import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react"; import {
ButtonHTMLAttributes,
Fragment,
ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { ReactSortable, ReactSortableProps } from "react-sortablejs"; import { ReactSortable, ReactSortableProps } from "react-sortablejs";
import { apiAuth, User } from "./api"; import { apiAuth, loadPlayers, User } from "./api";
import { TeamState, useSession } from "./Session"; import { TeamState, useSession } from "./Session";
import { Chemistry, MVPRanking, PlayerType } from "./types"; import { Chemistry, MVPRanking } from "./types";
import TabController from "./TabController"; import TabController from "./TabController";
type PlayerListProps = Partial<ReactSortableProps<any>> & { type PlayerListProps = Partial<ReactSortableProps<any>> & {
orderedList?: boolean; orderedList?: boolean;
gender?: boolean;
}; };
function PlayerList(props: PlayerListProps) { function PlayerList(props: PlayerListProps) {
const fmps = props.list?.filter((item) => item.gender === "fmp").length;
return ( return (
<ReactSortable <ReactSortable {...props} animation={200} swapThreshold={0.4}>
{...props} {props.list?.map((item, index) => (
animation={200} <div key={item.id} className="item">
swapThreshold={0.2} {props.orderedList
style={{ minHeight: props.list && props.list?.length < 1 ? 64 : 32 }} ? index + 1 + ". " + item.display_name
> : item.display_name}
{props.list && </div>
props.list.map((item, index) => ( ))}
<div
key={item.id}
className={"item " + (props.gender ? item.gender : "")}
>
{props.orderedList
? props.gender
? index +
1 -
(item.gender !== "fmp" ? fmps! : 0) +
". " +
item.display_name
: index + 1 + ". " + item.display_name
: item.display_name}
</div>
))}
</ReactSortable> </ReactSortable>
); );
} }
@ -75,11 +65,15 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
const [playersLeft, setPlayersLeft] = useState<User[]>([]); const [playersLeft, setPlayersLeft] = useState<User[]>([]);
const [playersMiddle, setPlayersMiddle] = useState<User[]>(otherPlayers); const [playersMiddle, setPlayersMiddle] = useState<User[]>(otherPlayers);
const [playersRight, setPlayersRight] = useState<User[]>([]); const [playersRight, setPlayersRight] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
handleGet(); setPlayersMiddle(otherPlayers);
}, [players]); }, [players]);
useEffect(() => {
setPlayersLeft([]);
setPlayersMiddle(otherPlayers);
setPlayersRight([]);
}, [teams]);
const [dialog, setDialog] = useState("dialog"); const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null); const dialogRef = useRef<HTMLDialogElement>(null);
@ -102,14 +96,9 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
} }
async function handleGet() { async function handleGet() {
setLoading(true);
const data = await apiAuth(`chemistry/${teams.activeTeam}`, null, "GET"); const data = await apiAuth(`chemistry/${teams.activeTeam}`, null, "GET");
if (data.detail) { if (data.detail) alert(data.detail);
console.log(data.detail); else {
setPlayersRight([]);
setPlayersMiddle(otherPlayers);
setPlayersLeft([]);
} else {
const chemistry = data as Chemistry; const chemistry = data as Chemistry;
setPlayersLeft(filterSort(otherPlayers, chemistry.hate)); setPlayersLeft(filterSort(otherPlayers, chemistry.hate));
setPlayersMiddle( setPlayersMiddle(
@ -121,7 +110,6 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
); );
setPlayersRight(filterSort(otherPlayers, chemistry.love)); setPlayersRight(filterSort(otherPlayers, chemistry.love));
} }
setLoading(false);
} }
return ( return (
@ -134,50 +122,46 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
setPlayersLeft([]); setPlayersLeft([]);
}} }}
/> />
{loading ? ( <div className="container">
<span className="loader" style={{ width: 300 }} /> <div className="box three">
) : ( <h2>😬</h2>
<div className="container"> {playersLeft.length < 1 && (
<div className="box three"> <span className="grey hint">
<h2>😬</h2> drag people here that you'd rather not play with
{playersLeft.length < 1 && ( </span>
<span className="grey hint"> )}
drag people here that you'd rather not play with <PlayerList
</span> list={playersLeft}
)} setList={setPlayersLeft}
<PlayerList group={"shared"}
list={playersLeft} className="dragbox"
setList={setPlayersLeft} />
group={"shared"}
className="dragbox"
/>
</div>
<div className="box three">
<h2>🤷</h2>
<PlayerList
list={playersMiddle}
setList={setPlayersMiddle}
group={"shared"}
className="middle dragbox"
/>
</div>
<div className="box three">
<h2>😍</h2>
{playersRight.length < 1 && (
<span className="grey hint">
drag people here that you love playing with from best to ... ok
</span>
)}
<PlayerList
list={playersRight}
setList={setPlayersRight}
group={"shared"}
className="dragbox"
orderedList
/>
</div>
</div> </div>
)} <div className="box three">
<h2>🤷</h2>
<PlayerList
list={playersMiddle}
setList={setPlayersMiddle}
group={"shared"}
className="middle dragbox"
/>
</div>
<div className="box three">
<h2>😍</h2>
{playersRight.length < 1 && (
<span className="grey hint">
drag people here that you love playing with from best to ... ok
</span>
)}
<PlayerList
list={playersRight}
setList={setPlayersRight}
group={"shared"}
className="dragbox"
orderedList
/>
</div>
</div>
<button className="submit wavering" onClick={() => handleSubmit()}> <button className="submit wavering" onClick={() => handleSubmit()}>
💾 <span className="submit_text">submit</span> 💾 <span className="submit_text">submit</span>
@ -195,160 +179,18 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
); );
} }
function TypeDnD({ user, teams, players }: PlayerInfoProps) {
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
const [handlers, setHandlers] = useState<User[]>([]);
const [combis, setCombis] = useState<User[]>([]);
const [cutters, setCutters] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
handleGet();
}, [players]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
async function handleSubmit() {
if (dialogRef.current) dialogRef.current.showModal();
setDialog("sending...");
let handlerlist = handlers.map(({ id }) => id);
let combilist = combis.map(({ id }) => id);
let cutterlist = cutters.map(({ id }) => id);
const data = {
user: user.id,
handlers: handlerlist,
combis: combilist,
cutters: cutterlist,
team: teams.activeTeam,
};
const response = await apiAuth("playertype", data, "PUT");
setDialog(response || "try sending again");
}
async function handleGet() {
setLoading(true);
const data = await apiAuth(`playertype/${teams.activeTeam}`, null, "GET");
if (data.detail) {
console.log(data.detail);
setAvailablePlayers(players);
setHandlers([]);
setCombis([]);
setCutters([]);
} else {
const playertype = data as PlayerType;
setAvailablePlayers(
players.filter(
(player) =>
!playertype.handlers.includes(player.id) &&
!playertype.combis.includes(player.id) &&
!playertype.cutters.includes(player.id)
)
);
setHandlers(filterSort(players, playertype.handlers));
setCombis(filterSort(players, playertype.combis));
setCutters(filterSort(players, playertype.cutters));
}
setLoading(false);
}
return (
<>
<HeaderControl
onLoad={handleGet}
onClear={() => {
setAvailablePlayers(players);
setHandlers([]);
setCombis([]);
setCutters([]);
}}
/>
<div className="container">
<div className="box one">
<PlayerList
list={availablePlayers}
setList={setAvailablePlayers}
group={"type-shared"}
className="dragbox reservoir"
/>
</div>
</div>
<div className="container">
<div className="box three">
<h4>handler</h4>
{handlers.length < 1 && (
<span className="grey hint">
drag people here that you like to see as handlers
</span>
)}
<PlayerList
list={handlers}
setList={setHandlers}
group={"type-shared"}
className="dragbox"
/>
</div>
<div className="box three">
<h4>combi</h4>
{combis.length < 1 && (
<span className="grey hint">
drag people here that switch between handling and cutting
</span>
)}
<PlayerList
list={combis}
setList={setCombis}
group={"type-shared"}
className="middle dragbox"
/>
</div>
<div className="box three">
<h4>cutter</h4>
{cutters.length < 1 && (
<span className="grey hint">
drag people here that you think are the best cutters
</span>
)}
<PlayerList
list={cutters}
setList={setCutters}
group={"type-shared"}
className="dragbox"
/>
</div>
</div>
<button className="submit wavering" onClick={() => handleSubmit()}>
💾 <span className="submit_text">submit</span>
</button>
<dialog
ref={dialogRef}
id="PlayerTypeDialog"
onClick={(event) => {
event.currentTarget.close();
}}
>
{dialog}
</dialog>
</>
);
}
function MVPDnD({ user, teams, players }: PlayerInfoProps) { function MVPDnD({ user, teams, players }: PlayerInfoProps) {
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players); const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
const [rankedPlayers, setRankedPlayers] = useState<User[]>([]); const [rankedPlayers, setRankedPlayers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [mixed, setMixed] = useState(false);
useEffect(() => { useEffect(() => {
const activeTeam = teams.teams.find((team) => team.id == teams.activeTeam); setAvailablePlayers(players);
activeTeam && setMixed(activeTeam.mixed);
handleGet();
}, [players]); }, [players]);
useEffect(() => { useEffect(() => {
handleGet(); setAvailablePlayers(players);
// setMixedList(rankedPlayers); setRankedPlayers([]);
}, [mixed]); }, [teams]);
const [dialog, setDialog] = useState("dialog"); const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null); const dialogRef = useRef<HTMLDialogElement>(null);
@ -362,30 +204,16 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) {
response ? setDialog(response) : setDialog("try sending again"); response ? setDialog(response) : setDialog("try sending again");
} }
const setMixedList = (newList: User[]) =>
mixed
? setRankedPlayers(
newList.sort((a, b) =>
a.gender && b.gender ? a.gender.localeCompare(b.gender) : -1
)
)
: setRankedPlayers(newList);
async function handleGet() { async function handleGet() {
setLoading(true);
const data = await apiAuth(`mvps/${teams.activeTeam}`, null, "GET"); const data = await apiAuth(`mvps/${teams.activeTeam}`, null, "GET");
if (data.detail) { if (data.detail) alert(data.detail);
console.log(data.detail); else {
setAvailablePlayers(players);
setRankedPlayers([]);
} else {
const mvps = data as MVPRanking; const mvps = data as MVPRanking;
setMixedList(filterSort(players, mvps.mvps)); setRankedPlayers(filterSort(players, mvps.mvps));
setAvailablePlayers( setAvailablePlayers(
players.filter((user) => !mvps.mvps.includes(user.id)) players.filter((user) => !mvps.mvps.includes(user.id))
); );
} }
setLoading(false);
} }
return ( return (
@ -397,46 +225,46 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) {
setRankedPlayers([]); setRankedPlayers([]);
}} }}
/> />
{loading ? ( <div className="container">
<span className="loader" style={{ width: 300 }} /> <div className="box two">
) : ( <h2>🥏🏃</h2>
<div className="container"> {availablePlayers.length < 1 && (
<div className="box two"> <span className="grey hint">all sorted 👍</span>
<h2>🥏🏃</h2> )}
{availablePlayers.length < 1 && ( <PlayerList
<span className="grey hint">all sorted 👍</span> list={availablePlayers}
)} setList={setAvailablePlayers}
<PlayerList group={{
list={availablePlayers} name: "mvp-shared",
setList={setAvailablePlayers} pull: function (to) {
group={{ return to.el.classList.contains("putclone") ? "clone" : true;
name: "mvp-shared", },
}} }}
className="dragbox" className="dragbox"
gender={mixed} />
/>
</div>
<div className="box two">
<h1>🏆</h1>
{rankedPlayers.length < 1 && (
<span className="grey hint">
carefully place as many of the <i>Most Valuable Players</i>{" "}
(according to your humble opinion) in this box
</span>
)}
<PlayerList
list={rankedPlayers}
setList={setMixedList}
group={{
name: "mvp-shared",
}}
className="dragbox"
orderedList
gender={mixed}
/>
</div>
</div> </div>
)} <div className="box two">
<h1>🏆</h1>
{rankedPlayers.length < 1 && (
<span className="grey hint">
carefully place as many of the <i>Most Valuable Players</i>{" "}
(according to your humble opinion) in this box
</span>
)}
<PlayerList
list={rankedPlayers}
setList={setRankedPlayers}
group={{
name: "mvp-shared",
pull: function (to) {
return to.el.classList.contains("putclone") ? "clone" : true;
},
}}
className="dragbox"
orderedList
/>
</div>
</div>
<button className="submit wavering" onClick={() => handleSubmit()}> <button className="submit wavering" onClick={() => handleSubmit()}>
💾 <span className="submit_text">submit</span> 💾 <span className="submit_text">submit</span>
@ -462,8 +290,8 @@ function HeaderControl({ onLoad, onClear }: HeaderControlProps) {
return ( return (
<> <>
<div> <div>
<ClearButton onClick={onClear} />
<LoadButton onClick={onLoad} /> <LoadButton onClick={onLoad} />
<ClearButton onClick={onClear} />
</div> </div>
<div> <div>
<span className="grey"> <span className="grey">
@ -476,11 +304,17 @@ function HeaderControl({ onLoad, onClear }: HeaderControlProps) {
} }
export default function Rankings() { export default function Rankings() {
const { user, teams, players } = useSession(); const { user, teams } = useSession();
const [players, setPlayers] = useState<User[] | null>(null);
useEffect(() => {
if (teams) {
loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
}
}, [user, teams]);
const tabs = [ const tabs = [
{ id: "Chemistry", label: "🧪 Chemistry" }, { id: "Chemistry", label: "🧪 Chemistry" },
{ id: "Type", label: "🃏 Type" },
{ id: "MVP", label: "🏆 MVP" }, { id: "MVP", label: "🏆 MVP" },
]; ];
@ -489,7 +323,6 @@ export default function Rankings() {
{user && teams && players ? ( {user && teams && players ? (
<TabController tabs={tabs}> <TabController tabs={tabs}>
<ChemistryDnD {...{ user, teams, players }} /> <ChemistryDnD {...{ user, teams, players }} />
<TypeDnD {...{ user, teams, players }} />
<MVPDnD {...{ user, teams, players }} /> <MVPDnD {...{ user, teams, players }} />
</TabController> </TabController>
) : ( ) : (

View File

@ -5,7 +5,7 @@ import {
useEffect, useEffect,
useState, useState,
} from "react"; } from "react";
import { apiAuth, currentUser, loadPlayers, logout, User } from "./api"; import { apiAuth, currentUser, logout, User } from "./api";
import { Login } from "./Login"; import { Login } from "./Login";
import Header from "./Header"; import Header from "./Header";
import { Team } from "./types"; import { Team } from "./types";
@ -23,8 +23,6 @@ export interface Session {
user: User | null; user: User | null;
teams: TeamState | null; teams: TeamState | null;
setTeams: (teams: TeamState) => void; setTeams: (teams: TeamState) => void;
players: User[] | null;
reloadPlayers: () => void;
onLogout: () => void; onLogout: () => void;
} }
@ -32,8 +30,6 @@ const sessionContext = createContext<Session>({
user: null, user: null,
teams: null, teams: null,
setTeams: () => {}, setTeams: () => {},
players: null,
reloadPlayers: () => {},
onLogout: () => {}, onLogout: () => {},
}); });
@ -42,7 +38,6 @@ export function SessionProvider(props: SessionProviderProps) {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [teams, setTeams] = useState<TeamState | null>(null); const [teams, setTeams] = useState<TeamState | null>(null);
const [players, setPlayers] = useState<User[] | null>(null);
const [err, setErr] = useState<unknown>(null); const [err, setErr] = useState<unknown>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -65,19 +60,12 @@ export function SessionProvider(props: SessionProviderProps) {
if (teams) setTeams({ teams: teams, activeTeam: teams[0].id }); if (teams) setTeams({ teams: teams, activeTeam: teams[0].id });
} }
async function reloadPlayers() {
teams && loadPlayers(teams?.activeTeam).then((data) => setPlayers(data));
}
useEffect(() => { useEffect(() => {
loadUser(); loadUser();
}, []); }, []);
useEffect(() => { useEffect(() => {
loadTeam(); loadTeam();
}, [user]); }, [user]);
useEffect(() => {
reloadPlayers();
}, [teams]);
function onLogin(user: User) { function onLogin(user: User) {
setUser(user); setUser(user);
@ -108,9 +96,7 @@ export function SessionProvider(props: SessionProviderProps) {
content = <Login onLogin={onLogin} />; content = <Login onLogin={onLogin} />;
} else } else
content = ( content = (
<sessionContext.Provider <sessionContext.Provider value={{ user, teams, setTeams, onLogout }}>
value={{ user, teams, setTeams, players, reloadPlayers, onLogout }}
>
{children} {children}
</sessionContext.Provider> </sessionContext.Provider>
); );

View File

@ -1,6 +1,6 @@
import { jwtDecode, JwtPayload } from "jwt-decode"; import { jwtDecode, JwtPayload } from "jwt-decode";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { apiAuth, baseUrl, Gender, User } from "./api"; import { apiAuth, baseUrl, User } from "./api";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { Eye, EyeSlash } from "./Icons"; import { Eye, EyeSlash } from "./Icons";
import { useSession } from "./Session"; import { useSession } from "./Session";
@ -237,21 +237,6 @@ export const SetPassword = () => {
}} }}
/> />
</div> </div>
<div>
<label>gender</label>
<select
name="gender"
required
value={player.gender}
onChange={(e) => {
setPlayer({ ...player, gender: e.target.value as Gender });
}}
>
<option value={undefined}></option>
<option value="fmp">FMP</option>
<option value="mmp">MMP</option>
</select>
</div>
<div> <div>
<label>number (optional)</label> <label>number (optional)</label>
<input <input

View File

@ -1,29 +1,35 @@
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import { apiAuth, Gender, User } from "./api"; import { apiAuth, loadPlayers, User } from "./api";
import { useSession } from "./Session"; import { useSession } from "./Session";
import { ErrorState } from "./types"; import { ErrorState } from "./types";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import Calendar from "./Calendar";
const TeamPanel = () => { const TeamPanel = () => {
const { user, teams, players, reloadPlayers } = useSession(); const { user, teams } = useSession();
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) || user?.scopes.includes(`team:${teams?.activeTeam}`) ||
teams?.activeTeam === 42 || teams?.activeTeam === 42 ||
navigate("/", { replace: true }); navigate("/", { replace: true });
}, [user, teams]); }, [user]);
const newPlayerTemplate = { const newPlayerTemplate = {
id: 0, id: 0,
username: "", username: "",
display_name: "", display_name: "",
gender: undefined,
number: "", number: "",
email: "", email: "",
} as User; } as User;
const [error, setError] = useState<ErrorState>(); const [error, setError] = useState<ErrorState>();
const [players, setPlayers] = useState<User[] | null>(null);
const [player, setPlayer] = useState(newPlayerTemplate); const [player, setPlayer] = useState(newPlayerTemplate);
useEffect(() => {
if (teams) {
setError({ ok: true, message: "" });
loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
}
}, [teams]);
async function handleSubmit(e: FormEvent) { async function handleSubmit(e: FormEvent) {
e.preventDefault(); e.preventDefault();
if (teams) { if (teams) {
@ -32,14 +38,14 @@ const TeamPanel = () => {
if (r.detail) setError({ ok: false, message: r.detail }); if (r.detail) setError({ ok: false, message: r.detail });
else { else {
setError({ ok: true, message: r }); setError({ ok: true, message: r });
reloadPlayers(); loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
} }
} else { } else {
const r = await apiAuth(`player/${teams?.activeTeam}`, player, "PUT"); const r = await apiAuth(`player/${teams?.activeTeam}`, player, "PUT");
if (r.detail) setError({ ok: false, message: r.detail }); if (r.detail) setError({ ok: false, message: r.detail });
else { else {
setError({ ok: true, message: r }); setError({ ok: true, message: r });
reloadPlayers(); loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
} }
} }
} }
@ -59,7 +65,7 @@ const TeamPanel = () => {
else { else {
setError({ ok: true, message: r }); setError({ ok: true, message: r });
setPlayer(newPlayerTemplate); setPlayer(newPlayerTemplate);
reloadPlayers(); loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
} }
} }
} }
@ -87,22 +93,19 @@ const TeamPanel = () => {
justifyContent: "center", justifyContent: "center",
}} }}
> >
{players.map((p) => ( {players &&
<button players.map((p) => (
className={ <button
"team-player " + className="team-player"
p.gender + key={p.id}
(p.id === player.id ? " active-player" : "") onClick={() => {
} setPlayer(p);
key={p.id} setError({ ok: true, message: "" });
onClick={() => { }}
setPlayer(p); >
setError({ ok: true, message: "" }); {p.display_name}
}} </button>
> ))}
{p.display_name}
</button>
))}
<button <button
className="team-player new-player" className="team-player new-player"
key="add-player" key="add-player"
@ -129,10 +132,8 @@ const TeamPanel = () => {
onChange={(e) => { onChange={(e) => {
setPlayer({ setPlayer({
...player, ...player,
...(player.id === 0 && {
username: e.target.value.toLowerCase().replace(/\W/g, ""),
}),
display_name: e.target.value, display_name: e.target.value,
username: e.target.value.toLowerCase().replace(/\W/g, ""),
}); });
setError({ ok: true, message: "" }); setError({ ok: true, message: "" });
}} }}
@ -151,21 +152,6 @@ const TeamPanel = () => {
}} }}
/> />
</div> </div>
<div>
<label>gender</label>
<select
name="gender"
value={player.gender}
onChange={(e) => {
setPlayer({ ...player, gender: e.target.value as Gender });
setError({ ok: true, message: "" });
}}
>
<option value={undefined}></option>
<option value="fmp">FMP</option>
<option value="mmp">MMP</option>
</select>
</div>
<div> <div>
<label>number (optional)</label> <label>number (optional)</label>
<input <input
@ -216,7 +202,6 @@ const TeamPanel = () => {
)} )}
</form> </form>
</div> </div>
<Calendar playerId={player.id} />
</div> </div>
); );
} else <span className="loader" />; } else <span className="loader" />;

View File

@ -43,15 +43,12 @@ export async function apiAuth(
} }
} }
export type Gender = "fmp" | "mmp" | undefined;
export type User = { export type User = {
id: number; id: number;
username: string; username: string;
display_name: string; display_name: string;
email: string; email: string;
number: string; number: string;
gender: Gender;
scopes: string; scopes: string;
}; };

View File

@ -13,7 +13,7 @@ export default interface NetworkData {
} }
export interface PlayerRanking { export interface PlayerRanking {
p_id: number; name: string;
rank: number; rank: number;
std: number; std: number;
n: number; n: number;
@ -27,14 +27,6 @@ export interface Chemistry {
love: number[]; love: number[];
} }
export interface PlayerType {
id: number;
user: number;
handlers: number[];
combis: number[];
cutters: number[];
}
export interface MVPRanking { export interface MVPRanking {
id: number; id: number;
user: number; user: number;
@ -46,7 +38,6 @@ export interface Team {
name: string; name: string;
location: string; location: string;
country: string; country: string;
mixed: boolean;
} }
export type ErrorState = { export type ErrorState = {