diff --git a/cutt/player.py b/cutt/player.py index 9ac6f39..ce91d8c 100644 --- a/cutt/player.py +++ b/cutt/player.py @@ -1,19 +1,61 @@ from typing import Annotated -from fastapi import APIRouter, Depends, Security +from fastapi import APIRouter, Depends, HTTPException, Security, status from fastapi.responses import PlainTextResponse +from pydantic import BaseModel from sqlmodel import Session, select -from cutt.db import Player, Team, engine -from cutt.security import change_password, get_current_active_user, read_player_me +from cutt.db import Player, PlayerTeamLink, Team, engine +from cutt.security import ( + TeamScopedRequest, + change_password, + get_current_active_user, + read_player_me, + verify_team_scope, +) P = Player player_router = APIRouter(prefix="/player", tags=["player"]) -def add_player(player: P): +class AddPlayerRequest(BaseModel): + display_name: str + username: str + number: str + email: str + + +def add_player( + r: AddPlayerRequest, + request: Annotated[TeamScopedRequest, Depends(verify_team_scope)], +): with Session(engine) as session: - session.add(player) + if session.exec(select(P).where(P.username == r.username)).one_or_none(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="username not available" + ) + + stmt = ( + select(P) + .join(PlayerTeamLink) + .join(Team) + .where(Team.id == request.team_id, P.display_name == r.display_name) + ) + if session.exec(stmt).one_or_none(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="the name is already taken on this team", + ) + + team = session.exec(select(Team).where(Team.id == request.team_id)).one() + new_player = Player( + username=r.username, + display_name=r.display_name, + email=r.email if r.email else None, + number=r.number, + teams=[team], + ) + session.add(new_player) session.commit() @@ -48,8 +90,9 @@ async def list_players(team_id: int): players = [t.players for t in session.exec(statement)][0] if players: return [ - player.model_dump(include={"id", "display_name", "number"}) + player.model_dump(include={"id", "display_name", "username", "number"}) for player in players + if not player.disabled ] @@ -59,10 +102,9 @@ def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]): player_router.add_api_route( - "/add", + "/add/{team_id}", endpoint=add_player, methods=["POST"], - dependencies=[Security(get_current_active_user, scopes=["admin"])], ) player_router.add_api_route( "/list/{team_id}", diff --git a/src/App.css b/src/App.css index f45e125..f744301 100644 --- a/src/App.css +++ b/src/App.css @@ -480,6 +480,7 @@ button { /*========TEAM PANEL========*/ .team-panel { + max-width: 800px; padding: 1em; border: 3px solid black; box-shadow: 8px 8px black; @@ -492,44 +493,47 @@ button { } .team-player { - cursor: pointer; color: black; background-color: #36c4; border: 1px solid black; - border-radius: 1em; + border-radius: 1.4em; margin: 4px; - padding: 0 0.5em; + padding: 0.2em 0.5em; &:hover { background-color: #36c8; } + + &.new-player { + background-color: #3838; + } } .new-player-inputs { margin: auto; div { - display: flex; - flex-wrap: wrap; - margin: auto; + display: grid; + grid-template-columns: 20ch auto; + + @media only screen and (max-width: 768px) { + grid-template-columns: auto; + place-items: center; + } label { - text-align: right; - margin-left: 2em; - margin-right: auto; + text-align: left; + width: 20ch; + margin: auto 1em; } input { - width: 200px; - margin: 2px 1em; + width: 90%; + margin: 4px 0; } } } -#new-team-player { - background-color: #3838; - font-weight: bold; -} @keyframes blink { diff --git a/src/Login.tsx b/src/Login.tsx index 62aa361..4a336ef 100644 --- a/src/Login.tsx +++ b/src/Login.tsx @@ -114,7 +114,7 @@ export const Login = ({ onLogin }: LoginProps) => { {visible ? : } -
{error && {error}}
+ {error && {error}} diff --git a/src/TeamPanel.tsx b/src/TeamPanel.tsx index f973ba2..8074e79 100644 --- a/src/TeamPanel.tsx +++ b/src/TeamPanel.tsx @@ -1,21 +1,38 @@ -import { useEffect, useState } from "react"; -import { loadPlayers, User } from "./api"; +import { FormEvent, useEffect, useState } from "react"; +import { apiAuth, loadPlayers, User } from "./api"; import { useSession } from "./Session"; export const TeamPanel = () => { const { teams } = useSession(); - const [players, setPlayers] = useState(null); - const [newPlayer, setNewPlayer] = useState({ + const newPlayerTemplate = { + id: 0, username: "", display_name: "", number: "", - }); + email: "", + } as User; + const [error, setError] = useState(""); + const [players, setPlayers] = useState(null); + const [player, setPlayer] = useState(newPlayerTemplate); + useEffect(() => { if (teams) { loadPlayers(teams.activeTeam).then((data) => setPlayers(data)); } }, [teams]); + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + if (teams) { + const r = await apiAuth( + `player/add/${teams?.activeTeam}`, + player, + "POST" + ); + if (r.detail) setError(r.detail); + } + } + if (teams) { const activeTeam = teams.teams.filter( (team) => team.id == teams?.activeTeam @@ -23,36 +40,57 @@ export const TeamPanel = () => { return (

{activeTeam.name}

-
+
+

players

-
- {players && - players.map((player) => ( -
- {player.display_name} -
- ))} -
- + {players ? ( +
+ {players && + players.map((p) => ( + + ))} + +
+ ) : ( + + )}
-
+ +
- setNewPlayer({ ...newPlayer, display_name: e.target.value }) + setPlayer({ + ...player, + display_name: e.target.value, + username: e.target.value.toLowerCase().replace(/\W/g, ""), + }) } />
@@ -60,9 +98,10 @@ export const TeamPanel = () => { - setNewPlayer({ ...newPlayer, username: e.target.value }) + setPlayer({ ...player, username: e.target.value }) } />
@@ -70,16 +109,21 @@ export const TeamPanel = () => { - setNewPlayer({ ...newPlayer, number: e.target.value }) + setPlayer({ ...player, number: e.target.value }) } />
-
- +
+ {error && ( + {error} + )} +
+ +
);