Compare commits

..

No commits in common. "b9efd4f7a3f3318f0f15dc2c2cc10251c1a8031c" and "6902ffdca6f28584bca7c01ba124e3204742b6d2" have entirely different histories.

15 changed files with 153 additions and 540 deletions

View File

@ -1,6 +1,4 @@
import io import io
import itertools
import random
import base64 import base64
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, HTTPException, Security, status from fastapi import APIRouter, HTTPException, Security, status
@ -14,7 +12,6 @@ import numpy as np
import matplotlib import matplotlib
from cutt.security import TeamScopedRequest, verify_team_scope from cutt.security import TeamScopedRequest, verify_team_scope
from cutt.demo import demo_players
matplotlib.use("agg") matplotlib.use("agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -58,65 +55,14 @@ def sociogram_json():
def graph_json( def graph_json(
request: Annotated[TeamScopedRequest, Security(verify_team_scope)], request: Annotated[
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
],
networkx_graph: bool = False, networkx_graph: bool = False,
): ):
nodes = [] nodes = []
edges = [] edges = []
player_map = {} 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: with Session(engine) as session:
players = session.exec( players = session.exec(
select(P) select(P)
@ -294,26 +240,11 @@ async def render_sociogram(params: Params):
def mvp( def mvp(
request: Annotated[TeamScopedRequest, Security(verify_team_scope)], request: Annotated[
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
],
): ):
ranks = dict() 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: with Session(engine) as session:
players = session.exec( players = session.exec(
select(P) select(P)

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,
@ -49,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
@ -69,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

@ -1,28 +0,0 @@
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 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
@ -77,8 +76,6 @@ def submit_mvps(
mvps: MVPRanking, mvps: MVPRanking,
user: Annotated[Player, Depends(get_current_active_user)], user: Annotated[Player, Depends(get_current_active_user)],
): ):
if mvps.team == 42:
return JSONResponse("DEMO team, nothing happens")
if user.id == mvps.user: if user.id == mvps.user:
with Session(engine) as session: with Session(engine) as session:
statement = select(Team).where(Team.id == mvps.team) statement = select(Team).where(Team.id == mvps.team)
@ -115,7 +112,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",
) )
@ -124,8 +121,6 @@ def get_mvps(
def submit_chemistry( def submit_chemistry(
chemistry: Chemistry, user: Annotated[Player, Depends(get_current_active_user)] 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: if user.id == chemistry.user:
with Session(engine) as session: with Session(engine) as session:
statement = select(Team).where(Team.id == chemistry.team) statement = select(Team).where(Team.id == chemistry.team)
@ -159,61 +154,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

@ -12,7 +12,6 @@ from cutt.security import (
read_player_me, read_player_me,
verify_team_scope, verify_team_scope,
) )
from cutt.demo import demo_players
P = Player P = Player
@ -22,20 +21,13 @@ 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): ...
DEMO_TEAM_REQUEST = HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="DEMO Team, nothing happens",
)
def add_player( def add_player(
r: AddPlayerRequest, r: AddPlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
@ -62,7 +54,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,
@ -81,8 +72,6 @@ def modify_player(
r: ModifyPlayerRequest, r: ModifyPlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
): ):
if request.team_id == 42:
raise DEMO_TEAM_REQUEST
with Session(engine) as session: with Session(engine) as session:
player = session.exec( player = session.exec(
select(P) select(P)
@ -91,11 +80,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")
@ -114,8 +101,6 @@ def disable_player(
r: DisablePlayerRequest, r: DisablePlayerRequest,
request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
): ):
if request.team_id == 42:
raise DEMO_TEAM_REQUEST
with Session(engine) as session: with Session(engine) as session:
player = session.exec( player = session.exec(
select(P) select(P)
@ -163,13 +148,6 @@ async def list_all_players():
async def list_players( async def list_players(
team_id: int, user: Annotated[Player, Depends(get_current_active_user)] 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: with Session(engine) as session:
current_user = session.exec( current_user = session.exec(
select(P) select(P)
@ -192,14 +170,7 @@ async def list_players(
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
@ -208,9 +179,7 @@ async def list_players(
def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]): def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]):
with Session(engine) as session: 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( player_router.add_api_route(

View File

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

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,8 +151,7 @@ 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;
@ -178,6 +180,7 @@ h3 {
flex-direction: column; flex-direction: column;
} }
.container { .container {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -188,6 +191,7 @@ h3 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 32px; min-height: 32px;
height: 92%;
} }
.box { .box {
@ -197,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;
@ -210,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;
@ -281,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;
@ -306,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 {
@ -314,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;
} }
@ -487,10 +488,7 @@ button {
input { input {
max-width: 300px; max-width: 300px;
} margin: 0.2em auto;
select {
max-width: 335px;
background-color: white;
} }
} }
@ -513,12 +511,6 @@ button {
&.disable-player { &.disable-player {
background-color: #e338; background-color: #e338;
} }
&.mmp {
background-color: lightskyblue;
}
&.fmp {
background-color: salmon;
}
} }
.new-player-inputs { .new-player-inputs {
@ -541,19 +533,21 @@ button {
margin: auto 1em; margin: auto 1em;
} }
input, input {
select {
width: 90%; width: 90%;
margin: 4px 0; margin: 4px 0;
} }
} }
} }
@keyframes blink { @keyframes blink {
0% { 0% {
background-color: #8888; background-color: #8888;
} }
13% { 13% {
background-color: #8888; background-color: #8888;
} }
@ -566,6 +560,7 @@ button {
background-color: #8888; background-color: #8888;
} }
38% { 38% {
background-color: #8888; background-color: #8888;
} }

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>

View File

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

View File

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

View File

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

View File

@ -1,8 +1,15 @@
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, loadPlayers, 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>> & {
@ -11,12 +18,7 @@ type PlayerListProps = Partial<ReactSortableProps<any>> & {
function PlayerList(props: PlayerListProps) { function PlayerList(props: PlayerListProps) {
return ( return (
<ReactSortable <ReactSortable {...props} animation={200} swapThreshold={0.4}>
{...props}
animation={200}
swapThreshold={0.2}
style={{ minHeight: props.list?.length < 1 ? 64 : 32 }}
>
{props.list?.map((item, index) => ( {props.list?.map((item, index) => (
<div key={item.id} className="item"> <div key={item.id} className="item">
{props.orderedList {props.orderedList
@ -63,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);
@ -90,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(
@ -109,7 +110,6 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
); );
setPlayersRight(filterSort(otherPlayers, chemistry.love)); setPlayersRight(filterSort(otherPlayers, chemistry.love));
} }
setLoading(false);
} }
return ( return (
@ -122,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>
@ -183,153 +179,19 @@ 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);
useEffect(() => { useEffect(() => {
handleGet(); setAvailablePlayers(players);
}, [players]); }, [players]);
useEffect(() => {
setAvailablePlayers(players);
setRankedPlayers([]);
}, [teams]);
const [dialog, setDialog] = useState("dialog"); const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null); const dialogRef = useRef<HTMLDialogElement>(null);
@ -343,20 +205,15 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) {
} }
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;
setRankedPlayers(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 (
@ -368,50 +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", },
pull: function (to) { }}
return to.el.classList.contains("putclone") ? "clone" : true; className="dragbox"
}, />
}}
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>
)} <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>
@ -437,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">
@ -462,7 +315,6 @@ export default function Rankings() {
const tabs = [ const tabs = [
{ id: "Chemistry", label: "🧪 Chemistry" }, { id: "Chemistry", label: "🧪 Chemistry" },
{ id: "Type", label: "🃏 Type" },
{ id: "MVP", label: "🏆 MVP" }, { id: "MVP", label: "🏆 MVP" },
]; ];
@ -471,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

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

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

@ -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;