6 Commits

Author SHA1 Message Date
b9efd4f7a3 feat: add player type survey 2025-05-19 14:32:30 +02:00
a6ebc28d47 fix: gender and previous state for DEMO 2025-05-18 16:01:05 +02:00
48f282423f feat: add gender in DEMO 2025-05-18 13:19:50 +02:00
881e015c1f Merge branch 'feat/demo' 2025-05-18 13:18:46 +02:00
4e2e0dd2a5 feat: add gender 2025-05-18 13:18:02 +02:00
b739246129 feat: load previously submitted by default 2025-05-18 11:59:07 +02:00
10 changed files with 417 additions and 156 deletions

View File

@@ -1,6 +1,7 @@
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,
@@ -48,6 +49,7 @@ 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
@@ -67,6 +69,16 @@ 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,16 +2,16 @@ import random
from cutt.db import Player from cutt.db import Player
names = [ names = [
"August", ("August", "mmp"),
"Beate", ("Beate", "fmp"),
"Ceasar", ("Ceasar", "mmp"),
"Daedalus", ("Daedalus", "mmp"),
"Elli", ("Elli", "fmp"),
"Ford P.", ("Ford P.", ""),
"Gabriel", ("Gabriel", "mmp"),
"Hugo", ("Hugo", "mmp"),
"Ivar Johansson", ("Ivar Johansson", "mmp"),
"Jürgen Gordon Malinauskas", ("Jürgen Gordon Malinauskas", "mmp"),
] ]
demo_players = [ demo_players = [
Player.model_validate( Player.model_validate(
@@ -19,9 +19,10 @@ demo_players = [
"id": i, "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 in enumerate(names) 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, Team, Chemistry, MVPRanking, engine from cutt.db import Player, PlayerType, Team, Chemistry, MVPRanking, engine
from sqlmodel import ( from sqlmodel import (
Session, Session,
func, func,
@@ -20,6 +20,7 @@ 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
@@ -114,7 +115,7 @@ def get_mvps(
return mvps return mvps
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_400_BAD_REQUEST,
detail="no previous state was found", detail="no previous state was found",
) )
@@ -158,11 +159,61 @@ 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: if chemistry is not None:
return chemistry return chemistry
else: else:
raise HTTPException( 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", detail="no previous state was found",
) )

View File

@@ -22,8 +22,9 @@ 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 email: str | None
class AddPlayerRequest(PlayerRequest): ... class AddPlayerRequest(PlayerRequest): ...
@@ -61,6 +62,7 @@ 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,
@@ -89,9 +91,11 @@ 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.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.add(player)
session.commit() session.commit()
return PlainTextResponse("modification successful") return PlainTextResponse("modification successful")
@@ -162,7 +166,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", "username", "number", "email"} include={"id", "display_name", "gender", "username", "number", "email"}
) )
] + demo_players ] + demo_players
@@ -188,7 +192,14 @@ async def list_players(
if players: if players:
return [ return [
player.model_dump( player.model_dump(
include={"id", "display_name", "username", "number", "email"} include={
"id",
"display_name",
"username",
"gender",
"number",
"email",
}
) )
for player in players for player in players
if not player.disabled if not player.disabled

View File

@@ -29,7 +29,6 @@ dialog {
border-radius: 1em; border-radius: 1em;
} }
/*=========Network Controls=========*/ /*=========Network Controls=========*/
.infobutton { .infobutton {
@@ -61,7 +60,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 {
@@ -103,8 +102,8 @@ dialog {
bottom: 0; bottom: 0;
background-color: #ccc; background-color: #ccc;
border-radius: 34px; border-radius: 34px;
-webkit-transition: .4s; -webkit-transition: 0.4s;
transition: .4s; transition: 0.4s;
} }
.slider:before { .slider:before {
@@ -116,19 +115,19 @@ dialog {
bottom: 3px; bottom: 3px;
background-color: white; background-color: white;
border-radius: 50%; border-radius: 50%;
-webkit-transition: .4s; -webkit-transition: 0.4s;
transition: .4s; transition: 0.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);
@@ -138,8 +137,6 @@ input:checked+.slider:before {
opacity: 66%; opacity: 66%;
} }
.hint { .hint {
position: absolute; position: absolute;
font-size: 80%; font-size: 80%;
@@ -151,7 +148,8 @@ 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;
@@ -180,7 +178,6 @@ h3 {
flex-direction: column; flex-direction: column;
} }
.container { .container {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@@ -191,7 +188,6 @@ h3 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 32px; min-height: 32px;
height: 92%;
} }
.box { .box {
@@ -201,6 +197,9 @@ 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;
@@ -211,6 +210,7 @@ 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,10 +281,8 @@ 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;
@@ -308,7 +306,6 @@ button {
font-size: xx-large; font-size: xx-large;
margin-bottom: 16px; margin-bottom: 16px;
margin-right: 16px; margin-right: 16px;
} }
.wavering { .wavering {
@@ -317,11 +314,13 @@ button {
} }
::backdrop { ::backdrop {
background-image: linear-gradient(45deg, background-image: linear-gradient(
45deg,
magenta, magenta,
rebeccapurple, rebeccapurple,
dodgerblue, dodgerblue,
green); green
);
opacity: 0.75; opacity: 0.75;
} }
@@ -488,7 +487,10 @@ button {
input { input {
max-width: 300px; max-width: 300px;
margin: 0.2em auto; }
select {
max-width: 335px;
background-color: white;
} }
} }
@@ -511,6 +513,12 @@ 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 {
@@ -533,21 +541,19 @@ 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;
} }
@@ -560,7 +566,6 @@ button {
background-color: #8888; background-color: #8888;
} }
38% { 38% {
background-color: #8888; background-color: #8888;
} }

View File

@@ -22,6 +22,10 @@ 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

@@ -1,15 +1,8 @@
import { import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react";
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 } from "./types"; import { Chemistry, MVPRanking, PlayerType } from "./types";
import TabController from "./TabController"; import TabController from "./TabController";
type PlayerListProps = Partial<ReactSortableProps<any>> & { type PlayerListProps = Partial<ReactSortableProps<any>> & {
@@ -18,7 +11,12 @@ type PlayerListProps = Partial<ReactSortableProps<any>> & {
function PlayerList(props: PlayerListProps) { function PlayerList(props: PlayerListProps) {
return ( 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) => ( {props.list?.map((item, index) => (
<div key={item.id} className="item"> <div key={item.id} className="item">
{props.orderedList {props.orderedList
@@ -65,15 +63,11 @@ 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(() => {
setPlayersMiddle(otherPlayers); handleGet();
}, [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);
@@ -96,9 +90,14 @@ 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) alert(data.detail); if (data.detail) {
else { console.log(data.detail);
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(
@@ -110,6 +109,7 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
); );
setPlayersRight(filterSort(otherPlayers, chemistry.love)); setPlayersRight(filterSort(otherPlayers, chemistry.love));
} }
setLoading(false);
} }
return ( return (
@@ -122,6 +122,9 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
setPlayersLeft([]); setPlayersLeft([]);
}} }}
/> />
{loading ? (
<span className="loader" style={{ width: 300 }} />
) : (
<div className="container"> <div className="container">
<div className="box three"> <div className="box three">
<h2>😬</h2> <h2>😬</h2>
@@ -162,6 +165,7 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
/> />
</div> </div>
</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>
@@ -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) { 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(() => {
setAvailablePlayers(players); handleGet();
}, [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);
@@ -205,15 +343,20 @@ 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) alert(data.detail); if (data.detail) {
else { console.log(data.detail);
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 (
@@ -225,6 +368,9 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) {
setRankedPlayers([]); setRankedPlayers([]);
}} }}
/> />
{loading ? (
<span className="loader" style={{ width: 300 }} />
) : (
<div className="container"> <div className="container">
<div className="box two"> <div className="box two">
<h2>🥏🏃</h2> <h2>🥏🏃</h2>
@@ -265,6 +411,7 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) {
/> />
</div> </div>
</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>
@@ -290,8 +437,8 @@ function HeaderControl({ onLoad, onClear }: HeaderControlProps) {
return ( return (
<> <>
<div> <div>
<LoadButton onClick={onLoad} />
<ClearButton onClick={onClear} /> <ClearButton onClick={onClear} />
<LoadButton onClick={onLoad} />
</div> </div>
<div> <div>
<span className="grey"> <span className="grey">
@@ -315,6 +462,7 @@ 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" },
]; ];
@@ -323,6 +471,7 @@ 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, loadPlayers, User } from "./api"; import { apiAuth, Gender, 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";
@@ -16,6 +16,7 @@ const TeamPanel = () => {
id: 0, id: 0,
username: "", username: "",
display_name: "", display_name: "",
gender: undefined,
number: "", number: "",
email: "", email: "",
} as User; } as User;
@@ -96,7 +97,7 @@ const TeamPanel = () => {
{players && {players &&
players.map((p) => ( players.map((p) => (
<button <button
className="team-player" className={"team-player " + p.gender}
key={p.id} key={p.id}
onClick={() => { onClick={() => {
setPlayer(p); setPlayer(p);
@@ -152,6 +153,22 @@ 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,12 +43,15 @@ 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,6 +27,14 @@ 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;