diff --git a/main.py b/main.py index cf62c31..34b7f70 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,11 @@ -from fastapi import APIRouter, Depends, FastAPI, Security +from typing import Annotated +from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from db import Player, Team, Chemistry, MVPRanking, engine from sqlmodel import ( Session, + func, select, ) from fastapi.middleware.cors import CORSMiddleware @@ -18,6 +20,9 @@ from security import ( set_first_password, ) +C = Chemistry +R = MVPRanking +P = Player app = FastAPI(title="cutt") api_router = APIRouter(prefix="/api") @@ -80,20 +85,83 @@ team_router.add_api_route("/list", endpoint=list_teams, methods=["GET"]) team_router.add_api_route("/add", endpoint=add_team, methods=["POST"]) -@api_router.post("/mvps", dependencies=[Depends(get_current_active_user)]) -def submit_mvps(mvps: MVPRanking): - with Session(engine) as session: - session.add(mvps) - session.commit() - return JSONResponse("success!") +wrong_user_id_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="you're not who you think you are...", +) -@api_router.post("/chemistry", dependencies=[Depends(get_current_active_user)]) -def submit_chemistry(chemistry: Chemistry): +@api_router.put("/mvps") +def submit_mvps( + mvps: MVPRanking, + user: Annotated[Player, Depends(get_current_active_user)], +): + if user.id == mvps.user: + with Session(engine) as session: + session.add(mvps) + session.commit() + return JSONResponse("success!") + else: + raise wrong_user_id_exception + + +@api_router.get("/mvps") +def get_mvps( + user: Annotated[Player, Depends(get_current_active_user)], +): with Session(engine) as session: - session.add(chemistry) - session.commit() - return JSONResponse("success!") + subquery = ( + select(R.user, func.max(R.time).label("latest")) + .where(R.user == user.id) + .group_by(R.user) + .subquery() + ) + statement2 = select(R).join( + subquery, (R.user == subquery.c.user) & (R.time == subquery.c.latest) + ) + mvps = session.exec(statement2).one_or_none() + if mvps: + return mvps + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="no previous state was found", + ) + + +@api_router.put("/chemistry") +def submit_chemistry( + chemistry: Chemistry, user: Annotated[Player, Depends(get_current_active_user)] +): + if user.id == chemistry.user: + with Session(engine) as session: + session.add(chemistry) + session.commit() + return JSONResponse("success!") + else: + raise wrong_user_id_exception + + +@api_router.get("/chemistry") +def get_chemistry(user: Annotated[Player, Depends(get_current_active_user)]): + with Session(engine) as session: + subquery = ( + select(C.user, func.max(C.time).label("latest")) + .where(C.user == user.id) + .group_by(C.user) + .subquery() + ) + statement2 = select(C).join( + subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest) + ) + chemistry = session.exec(statement2).one_or_none() + if chemistry: + return chemistry + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="no previous state was found", + ) class SPAStaticFiles(StaticFiles): diff --git a/src/App.css b/src/App.css index b32ff23..d630e1e 100644 --- a/src/App.css +++ b/src/App.css @@ -235,15 +235,17 @@ h3 { margin: auto; } -button, -.button { +button { margin: 4px; font-weight: bold; - font-size: large; color: aliceblue; background-color: black; border-radius: 1.2em; z-index: 1; + + &:hover { + opacity: 75%; + } } #control-panel { diff --git a/src/Rankings.tsx b/src/Rankings.tsx index 2dae236..8648bfd 100644 --- a/src/Rankings.tsx +++ b/src/Rankings.tsx @@ -1,7 +1,8 @@ -import { useEffect, useRef, useState } from "react"; +import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react"; import { ReactSortable, ReactSortableProps } from "react-sortablejs"; import { apiAuth, User } from "./api"; import { useSession } from "./Session"; +import { Chemistry, MVPRanking } from "./types"; type PlayerListProps = Partial> & { orderedList?: boolean; @@ -21,15 +22,37 @@ function PlayerList(props: PlayerListProps) { ); } +const LoadButton = (props: ButtonHTMLAttributes) => { + return ( + + ); +}; + +const ClearButton = (props: ButtonHTMLAttributes) => { + return ( + + ); +}; + +function filterSort(list: User[], ids: number[]): User[] { + const objectMap = new Map(list.map((obj) => [obj.id, obj])); + const filteredAndSortedObjects = ids + .map((id) => objectMap.get(id)) + .filter((obj) => obj !== undefined); + return filteredAndSortedObjects; +} + interface PlayerInfoProps { user: User; players: User[]; } -export function Chemistry({ user, players }: PlayerInfoProps) { - const index = players.indexOf(user); - var otherPlayers = players.slice(); - otherPlayers.splice(index, 1); +function ChemistryDnD({ user, players }: PlayerInfoProps) { + var otherPlayers = players.filter((player) => player.id !== user.id); const [playersLeft, setPlayersLeft] = useState([]); const [playersMiddle, setPlayersMiddle] = useState(otherPlayers); const [playersRight, setPlayersRight] = useState([]); @@ -48,12 +71,27 @@ export function Chemistry({ user, players }: PlayerInfoProps) { let middle = playersMiddle.map(({ id }) => id); let right = playersRight.map(({ id }) => id); const data = { user: user.id, hate: left, undecided: middle, love: right }; - const response = await apiAuth("chemistry", data, "POST"); - response ? setDialog(response) : setDialog("try sending again"); + const response = await apiAuth("chemistry", data, "PUT"); + setDialog(response || "try sending again"); + } + + async function handleGet() { + const chemistry = (await apiAuth("chemistry", null, "GET")) as Chemistry; + setPlayersLeft(filterSort(otherPlayers, chemistry.hate)); + setPlayersMiddle(filterSort(otherPlayers, chemistry.undecided)); + setPlayersRight(filterSort(otherPlayers, chemistry.love)); } return ( <> + { + setPlayersRight([]); + setPlayersMiddle(otherPlayers); + setPlayersLeft([]); + }} + />

😬

@@ -111,7 +149,7 @@ export function Chemistry({ user, players }: PlayerInfoProps) { ); } -export function MVP({ user, players }: PlayerInfoProps) { +function MVPDnD({ user, players }: PlayerInfoProps) { const [availablePlayers, setAvailablePlayers] = useState(players); const [rankedPlayers, setRankedPlayers] = useState([]); @@ -127,12 +165,25 @@ export function MVP({ user, players }: PlayerInfoProps) { setDialog("sending..."); let mvps = rankedPlayers.map(({ id }) => id); const data = { user: user.id, mvps: mvps }; - const response = await apiAuth("mvps", data, "POST"); + const response = await apiAuth("mvps", data, "PUT"); response ? setDialog(response) : setDialog("try sending again"); } + async function handleGet() { + const mvps = (await apiAuth("mvps", null, "GET")) as MVPRanking; + setRankedPlayers(filterSort(players, mvps.mvps)); + setAvailablePlayers(players.filter((user) => !mvps.mvps.includes(user.id))); + } + return ( <> + { + setAvailablePlayers(players); + setRankedPlayers([]); + }} + />

🥏🏃

@@ -190,6 +241,27 @@ export function MVP({ user, players }: PlayerInfoProps) { ); } +interface HeaderControlProps { + onLoad: () => void; + onClear: () => void; +} +function HeaderControl({ onLoad, onClear }: HeaderControlProps) { + return ( + <> +
+ + +
+
+ + assign as many or as few players as you want and don't forget to{" "} + submit 💾 when you're done :) + +
+ + ); +} + export default function Rankings() { const { user } = useSession(); const [players, setPlayers] = useState(null); @@ -230,18 +302,13 @@ export default function Rankings() { ))}
- - assign as many or as few players as you want -
- and don't forget to submit 💾 when you're done :) -
{tabs.map((tab) => { if (openTab !== tab.id) return null; switch (tab.id) { case "Chemistry": - return ; + return ; case "MVP": - return ; + return ; default: return null; } diff --git a/src/api.ts b/src/api.ts index fe5f485..1d19690 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,3 +1,5 @@ +import { useSession } from "./Session"; + export const baseUrl = import.meta.env.VITE_BASE_URL as string; export async function apiAuth( @@ -22,7 +24,8 @@ export async function apiAuth( if (!resp.ok) { if (resp.status === 401) { - logout(); + const { onLogout } = useSession(); + onLogout(); throw new Error("Unauthorized"); } } diff --git a/src/types.ts b/src/types.ts index 223565e..a0951c9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,3 +18,17 @@ export interface PlayerRanking { std: number; n: number; } + +export interface Chemistry { + id: number; + user: number; + hate: number[]; + undecided: number[]; + love: number[]; +} + +export interface MVPRanking { + id: number; + user: number; + mvps: number[]; +}