Compare commits

...

9 Commits

15 changed files with 540 additions and 153 deletions

View File

@ -1,4 +1,6 @@
import io
import itertools
import random
import base64
from typing import Annotated
from fastapi import APIRouter, HTTPException, Security, status
@ -12,6 +14,7 @@ import numpy as np
import matplotlib
from cutt.security import TeamScopedRequest, verify_team_scope
from cutt.demo import demo_players
matplotlib.use("agg")
import matplotlib.pyplot as plt
@ -55,14 +58,65 @@ def sociogram_json():
def graph_json(
request: Annotated[
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
],
request: Annotated[TeamScopedRequest, Security(verify_team_scope)],
networkx_graph: bool = False,
):
nodes = []
edges = []
player_map = {}
if request.team_id == 42:
players = [request.user] + demo_players
random.seed(42)
for p in players:
nodes.append({"id": p.display_name, "label": p.display_name})
for p, other in itertools.permutations(players, 2):
value = random.random()
if value > 0.5:
edges.append(
{
"id": f"{p.display_name}->{other.display_name}",
"source": p.display_name,
"target": other.display_name,
"size": max(value, 0.3),
"data": {
"relation": 2,
"origSize": max(value, 0.3),
"origFill": "#bed4ff",
},
}
)
elif value < 0.1:
edges.append(
{
"id": f"{p.display_name}-x>{other.display_name}",
"source": p.display_name,
"target": other.display_name,
"size": 0.3,
"data": {"relation": 0, "origSize": 0.3, "origFill": "#ff7c7c"},
"fill": "#ff7c7c",
}
)
G = nx.DiGraph()
G.add_nodes_from([n["id"] for n in nodes])
G.add_weighted_edges_from(
[
(
e["source"],
e["target"],
e["size"] if e["data"]["relation"] == 2 else -e["size"],
)
for e in edges
]
)
in_degrees = G.in_degree(weight="weight")
nodes = [
dict(node, **{"data": {"inDegree": in_degrees[node["id"]]}})
for node in nodes
]
if networkx_graph:
return G
return JSONResponse({"nodes": nodes, "edges": edges})
with Session(engine) as session:
players = session.exec(
select(P)
@ -240,11 +294,26 @@ async def render_sociogram(params: Params):
def mvp(
request: Annotated[
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
],
request: Annotated[TeamScopedRequest, Security(verify_team_scope)],
):
ranks = dict()
if request.team_id == 42:
random.seed(42)
players = [request.user] + demo_players
for p in players:
random.shuffle(players)
for i, p in enumerate(players):
ranks[p.display_name] = ranks.get(p.display_name, []) + [i + 1]
return [
{
"name": p,
"rank": f"{np.mean(v):.02f}",
"std": f"{np.std(v):.02f}",
"n": len(v),
}
for p, v in ranks.items()
]
with Session(engine) as session:
players = session.exec(
select(P)

View File

@ -1,6 +1,7 @@
from datetime import datetime, timezone
from sqlmodel import (
ARRAY,
CHAR,
Column,
Integer,
Relationship,
@ -48,6 +49,7 @@ class Player(SQLModel, table=True):
display_name: str
email: str | None = None
full_name: str | None = None
gender: str | None = Field(default=None, sa_column=Column(CHAR(3)))
disabled: bool | None = None
hashed_password: str | None = None
number: str | None = None
@ -67,6 +69,16 @@ class Chemistry(SQLModel, table=True):
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):
id: int | None = Field(default=None, primary_key=True)
time: datetime | None = Field(default_factory=utctime)

28
cutt/demo.py Normal file
View File

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

View File

@ -2,7 +2,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from cutt.db import Player, Team, Chemistry, MVPRanking, engine
from cutt.db import Player, PlayerType, Team, Chemistry, MVPRanking, engine
from sqlmodel import (
Session,
func,
@ -20,6 +20,7 @@ from cutt.security import (
from cutt.player import player_router
C = Chemistry
PT = PlayerType
R = MVPRanking
P = Player
@ -76,6 +77,8 @@ def submit_mvps(
mvps: MVPRanking,
user: Annotated[Player, Depends(get_current_active_user)],
):
if mvps.team == 42:
return JSONResponse("DEMO team, nothing happens")
if user.id == mvps.user:
with Session(engine) as session:
statement = select(Team).where(Team.id == mvps.team)
@ -112,7 +115,7 @@ def get_mvps(
return mvps
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST,
detail="no previous state was found",
)
@ -121,6 +124,8 @@ def get_mvps(
def submit_chemistry(
chemistry: Chemistry, user: Annotated[Player, Depends(get_current_active_user)]
):
if chemistry.team == 42:
return JSONResponse("DEMO team, nothing happens")
if user.id == chemistry.user:
with Session(engine) as session:
statement = select(Team).where(Team.id == chemistry.team)
@ -154,11 +159,61 @@ def get_chemistry(
subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest)
)
chemistry = session.exec(statement2).one_or_none()
if chemistry:
if chemistry is not None:
return chemistry
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST,
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",
)

View File

@ -12,6 +12,7 @@ from cutt.security import (
read_player_me,
verify_team_scope,
)
from cutt.demo import demo_players
P = Player
@ -21,13 +22,20 @@ player_router = APIRouter(prefix="/player", tags=["player"])
class PlayerRequest(BaseModel):
display_name: str
username: str
gender: str | None
number: str
email: str
email: str | None
class AddPlayerRequest(PlayerRequest): ...
DEMO_TEAM_REQUEST = HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="DEMO Team, nothing happens",
)
def add_player(
r: AddPlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
@ -54,6 +62,7 @@ def add_player(
new_player = Player(
username=r.username,
display_name=r.display_name,
gender=r.gender if r.gender else None,
email=r.email if r.email else None,
number=r.number,
disabled=False,
@ -72,6 +81,8 @@ def modify_player(
r: ModifyPlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
):
if request.team_id == 42:
raise DEMO_TEAM_REQUEST
with Session(engine) as session:
player = session.exec(
select(P)
@ -80,9 +91,11 @@ def modify_player(
.where(Team.id == request.team_id, P.id == r.id, P.username == r.username)
).one_or_none()
if player:
print(r)
player.display_name = r.display_name.strip()
player.number = r.number.strip()
player.email = r.email.strip()
player.gender = r.gender.strip() if r.gender else None
player.email = r.email.strip() if r.email else None
session.add(player)
session.commit()
return PlainTextResponse("modification successful")
@ -101,6 +114,8 @@ def disable_player(
r: DisablePlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
):
if request.team_id == 42:
raise DEMO_TEAM_REQUEST
with Session(engine) as session:
player = session.exec(
select(P)
@ -148,6 +163,13 @@ async def list_all_players():
async def list_players(
team_id: int, user: Annotated[Player, Depends(get_current_active_user)]
):
if team_id == 42:
return [
user.model_dump(
include={"id", "display_name", "gender", "username", "number", "email"}
)
] + demo_players
with Session(engine) as session:
current_user = session.exec(
select(P)
@ -170,7 +192,14 @@ async def list_players(
if players:
return [
player.model_dump(
include={"id", "display_name", "username", "number", "email"}
include={
"id",
"display_name",
"username",
"gender",
"number",
"email",
}
)
for player in players
if not player.disabled
@ -179,7 +208,9 @@ async def list_players(
def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]):
with Session(engine) as session:
return [p.teams for p in session.exec(select(P).where(P.id == user.id))][0]
return [p.teams for p in session.exec(select(P).where(P.id == user.id))][0] + [
{"country": "nowhere", "id": 42, "location": "everywhere", "name": "DEMO"}
]
player_router.add_api_route(

View File

@ -170,6 +170,8 @@ class TeamScopedRequest(BaseModel):
async def verify_team_scope(
team_id: int, user: Annotated[Player, Depends(get_current_active_user)]
):
if team_id == 42:
return TeamScopedRequest(user=user, team_id=team_id)
allowed_scopes = set(user.scopes.split())
if f"team:{team_id}" not in allowed_scopes:
raise HTTPException(

View File

@ -29,7 +29,6 @@ dialog {
border-radius: 1em;
}
/*=========Network Controls=========*/
.infobutton {
@ -61,7 +60,7 @@ dialog {
flex-wrap: wrap;
max-width: 240px;
margin: 0px;
background-color: #F0F8FFdd;
background-color: #f0f8ffdd;
.slider,
span {
@ -103,8 +102,8 @@ dialog {
bottom: 0;
background-color: #ccc;
border-radius: 34px;
-webkit-transition: .4s;
transition: .4s;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
@ -116,19 +115,19 @@ dialog {
bottom: 3px;
background-color: white;
border-radius: 50%;
-webkit-transition: .4s;
transition: .4s;
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked+.slider {
background-color: #2196F3;
input:checked + .slider {
background-color: #2196f3;
}
input:focus+.slider {
box-shadow: 0 0 1px #2196F3;
input:focus + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked+.slider:before {
input:checked + .slider:before {
-webkit-transform: translateX(24px);
-ms-transform: translateX(24px);
transform: translateX(24px);
@ -138,8 +137,6 @@ input:checked+.slider:before {
opacity: 66%;
}
.hint {
position: absolute;
font-size: 80%;
@ -151,7 +148,8 @@ input:checked+.slider:before {
z-index: -1;
}
input {
input,
select {
padding: 0.2em 16px;
margin-top: 0.25em;
margin-bottom: 0.25em;
@ -180,7 +178,6 @@ h3 {
flex-direction: column;
}
.container {
display: flex;
flex-wrap: nowrap;
@ -191,7 +188,6 @@ h3 {
display: flex;
flex-direction: column;
min-height: 32px;
height: 92%;
}
.box {
@ -201,6 +197,9 @@ h3 {
border-style: solid;
border-radius: 16px;
h4 {
margin: 4px;
}
&.one {
max-width: min(96%, 768px);
margin: 4px auto;
@ -211,6 +210,7 @@ h3 {
}
.reservoir {
display: flex;
flex-direction: unset;
flex-wrap: wrap;
justify-content: space-around;
@ -281,10 +281,8 @@ button {
font-size: 80%;
margin: 0px;
}
}
@media only screen and (max-width: 768px) {
#control-panel {
grid-template-columns: 1fr;
@ -308,7 +306,6 @@ button {
font-size: xx-large;
margin-bottom: 16px;
margin-right: 16px;
}
.wavering {
@ -317,11 +314,13 @@ button {
}
::backdrop {
background-image: linear-gradient(45deg,
magenta,
rebeccapurple,
dodgerblue,
green);
background-image: linear-gradient(
45deg,
magenta,
rebeccapurple,
dodgerblue,
green
);
opacity: 0.75;
}
@ -488,7 +487,10 @@ button {
input {
max-width: 300px;
margin: 0.2em auto;
}
select {
max-width: 335px;
background-color: white;
}
}
@ -511,6 +513,12 @@ button {
&.disable-player {
background-color: #e338;
}
&.mmp {
background-color: lightskyblue;
}
&.fmp {
background-color: salmon;
}
}
.new-player-inputs {
@ -533,21 +541,19 @@ button {
margin: auto 1em;
}
input {
input,
select {
width: 90%;
margin: 4px 0;
}
}
}
@keyframes blink {
0% {
background-color: #8888;
}
13% {
background-color: #8888;
}
@ -560,7 +566,6 @@ button {
background-color: #8888;
}
38% {
background-color: #8888;
}

View File

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

View File

@ -4,10 +4,11 @@ import { useSession } from "./Session";
export default function Footer() {
const location = useLocation();
const { user } = useSession();
const { user, teams } = useSession();
return (
<footer className={location.pathname === "/network" ? "fixed-footer" : ""}>
{user?.scopes.split(" ").includes("analysis") && (
{(user?.scopes.split(" ").includes("analysis") ||
teams?.activeTeam === 42) && (
<div className="navbar">
<Link to="/">
<span>Form</span>

View File

@ -15,6 +15,7 @@ const MVPChart = () => {
const navigate = useNavigate();
useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) ||
teams?.activeTeam === 42 ||
navigate("/", { replace: true });
}, [user]);

View File

@ -50,6 +50,7 @@ export const GraphComponent = () => {
const navigate = useNavigate();
useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) ||
teams?.activeTeam === 42 ||
navigate("/", { replace: true });
}, [user]);

View File

@ -1,15 +1,8 @@
import {
ButtonHTMLAttributes,
Fragment,
ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react";
import { ReactSortable, ReactSortableProps } from "react-sortablejs";
import { apiAuth, loadPlayers, User } from "./api";
import { TeamState, useSession } from "./Session";
import { Chemistry, MVPRanking } from "./types";
import { Chemistry, MVPRanking, PlayerType } from "./types";
import TabController from "./TabController";
type PlayerListProps = Partial<ReactSortableProps<any>> & {
@ -18,7 +11,12 @@ type PlayerListProps = Partial<ReactSortableProps<any>> & {
function PlayerList(props: PlayerListProps) {
return (
<ReactSortable {...props} animation={200} swapThreshold={0.4}>
<ReactSortable
{...props}
animation={200}
swapThreshold={0.2}
style={{ minHeight: props.list?.length < 1 ? 64 : 32 }}
>
{props.list?.map((item, index) => (
<div key={item.id} className="item">
{props.orderedList
@ -65,15 +63,11 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
const [playersLeft, setPlayersLeft] = useState<User[]>([]);
const [playersMiddle, setPlayersMiddle] = useState<User[]>(otherPlayers);
const [playersRight, setPlayersRight] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setPlayersMiddle(otherPlayers);
handleGet();
}, [players]);
useEffect(() => {
setPlayersLeft([]);
setPlayersMiddle(otherPlayers);
setPlayersRight([]);
}, [teams]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
@ -96,9 +90,14 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
}
async function handleGet() {
setLoading(true);
const data = await apiAuth(`chemistry/${teams.activeTeam}`, null, "GET");
if (data.detail) alert(data.detail);
else {
if (data.detail) {
console.log(data.detail);
setPlayersRight([]);
setPlayersMiddle(otherPlayers);
setPlayersLeft([]);
} else {
const chemistry = data as Chemistry;
setPlayersLeft(filterSort(otherPlayers, chemistry.hate));
setPlayersMiddle(
@ -110,6 +109,7 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
);
setPlayersRight(filterSort(otherPlayers, chemistry.love));
}
setLoading(false);
}
return (
@ -122,46 +122,50 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
setPlayersLeft([]);
}}
/>
<div className="container">
<div className="box three">
<h2>😬</h2>
{playersLeft.length < 1 && (
<span className="grey hint">
drag people here that you'd rather not play with
</span>
)}
<PlayerList
list={playersLeft}
setList={setPlayersLeft}
group={"shared"}
className="dragbox"
/>
{loading ? (
<span className="loader" style={{ width: 300 }} />
) : (
<div className="container">
<div className="box three">
<h2>😬</h2>
{playersLeft.length < 1 && (
<span className="grey hint">
drag people here that you'd rather not play with
</span>
)}
<PlayerList
list={playersLeft}
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 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()}>
💾 <span className="submit_text">submit</span>
@ -179,19 +183,153 @@ 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) {
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
const [rankedPlayers, setRankedPlayers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setAvailablePlayers(players);
handleGet();
}, [players]);
useEffect(() => {
setAvailablePlayers(players);
setRankedPlayers([]);
}, [teams]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
@ -205,15 +343,20 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) {
}
async function handleGet() {
setLoading(true);
const data = await apiAuth(`mvps/${teams.activeTeam}`, null, "GET");
if (data.detail) alert(data.detail);
else {
if (data.detail) {
console.log(data.detail);
setAvailablePlayers(players);
setRankedPlayers([]);
} else {
const mvps = data as MVPRanking;
setRankedPlayers(filterSort(players, mvps.mvps));
setAvailablePlayers(
players.filter((user) => !mvps.mvps.includes(user.id))
);
}
setLoading(false);
}
return (
@ -225,46 +368,50 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) {
setRankedPlayers([]);
}}
/>
<div className="container">
<div className="box two">
<h2>🥏🏃</h2>
{availablePlayers.length < 1 && (
<span className="grey hint">all sorted 👍</span>
)}
<PlayerList
list={availablePlayers}
setList={setAvailablePlayers}
group={{
name: "mvp-shared",
pull: function (to) {
return to.el.classList.contains("putclone") ? "clone" : true;
},
}}
className="dragbox"
/>
{loading ? (
<span className="loader" style={{ width: 300 }} />
) : (
<div className="container">
<div className="box two">
<h2>🥏🏃</h2>
{availablePlayers.length < 1 && (
<span className="grey hint">all sorted 👍</span>
)}
<PlayerList
list={availablePlayers}
setList={setAvailablePlayers}
group={{
name: "mvp-shared",
pull: function (to) {
return to.el.classList.contains("putclone") ? "clone" : true;
},
}}
className="dragbox"
/>
</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>
<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()}>
💾 <span className="submit_text">submit</span>
@ -290,8 +437,8 @@ function HeaderControl({ onLoad, onClear }: HeaderControlProps) {
return (
<>
<div>
<LoadButton onClick={onLoad} />
<ClearButton onClick={onClear} />
<LoadButton onClick={onLoad} />
</div>
<div>
<span className="grey">
@ -315,6 +462,7 @@ export default function Rankings() {
const tabs = [
{ id: "Chemistry", label: "🧪 Chemistry" },
{ id: "Type", label: "🃏 Type" },
{ id: "MVP", label: "🏆 MVP" },
];
@ -323,6 +471,7 @@ export default function Rankings() {
{user && teams && players ? (
<TabController tabs={tabs}>
<ChemistryDnD {...{ user, teams, players }} />
<TypeDnD {...{ user, teams, players }} />
<MVPDnD {...{ user, teams, players }} />
</TabController>
) : (

View File

@ -1,5 +1,5 @@
import { FormEvent, useEffect, useState } from "react";
import { apiAuth, loadPlayers, User } from "./api";
import { apiAuth, Gender, loadPlayers, User } from "./api";
import { useSession } from "./Session";
import { ErrorState } from "./types";
import { useNavigate } from "react-router";
@ -9,12 +9,14 @@ const TeamPanel = () => {
const navigate = useNavigate();
useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) ||
teams?.activeTeam === 42 ||
navigate("/", { replace: true });
}, [user]);
const newPlayerTemplate = {
id: 0,
username: "",
display_name: "",
gender: undefined,
number: "",
email: "",
} as User;
@ -95,7 +97,7 @@ const TeamPanel = () => {
{players &&
players.map((p) => (
<button
className="team-player"
className={"team-player " + p.gender}
key={p.id}
onClick={() => {
setPlayer(p);
@ -151,6 +153,22 @@ const TeamPanel = () => {
}}
/>
</div>
<div>
<label>gender</label>
<select
name="gender"
required
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>
<label>number (optional)</label>
<input

View File

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

View File

@ -27,6 +27,14 @@ export interface Chemistry {
love: number[];
}
export interface PlayerType {
id: number;
user: number;
handlers: number[];
combis: number[];
cutters: number[];
}
export interface MVPRanking {
id: number;
user: number;