feat: improve TeamPanel input placement

This commit is contained in:
julius 2025-03-24 11:57:51 +01:00
parent 691b99daa8
commit c04a1e03f2
Signed by: julius
GPG Key ID: C80A63E6A5FD7092
4 changed files with 146 additions and 56 deletions

View File

@ -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}",

View File

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

View File

@ -114,7 +114,7 @@ export const Login = ({ onLogin }: LoginProps) => {
{visible ? <Eye /> : <EyeSlash />}
</div>
</div>
<div>{error && <span style={{ color: "red" }}>{error}</span>}</div>
{error && <span style={{ color: "red" }}>{error}</span>}
<button type="submit" value="login" style={{ fontSize: "small" }}>
login
</button>

View File

@ -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<User[] | null>(null);
const [newPlayer, setNewPlayer] = useState({
const newPlayerTemplate = {
id: 0,
username: "",
display_name: "",
number: "",
});
email: "",
} as User;
const [error, setError] = useState("");
const [players, setPlayers] = useState<User[] | null>(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 (
<div className="team-panel">
<h1>{activeTeam.name}</h1>
<div className="stack column">
<div>
<input type="text" value={activeTeam.location || ""} disabled />
<br />
<input type="text" value={activeTeam.country || ""} disabled />
<hr style={{ width: "100%" }} />
<h2>players</h2>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
}}
>
{players &&
players.map((player) => (
<div className="team-player" key={player.id}>
{player.display_name}
</div>
))}
</div>
{players ? (
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
}}
>
{players &&
players.map((p) => (
<button
className="team-player"
key={p.id}
onClick={() => setPlayer(p)}
>
{p.display_name}
</button>
))}
<button
className="team-player new-player"
key="add-player"
onClick={() => setPlayer(newPlayerTemplate)}
>
+
</button>
</div>
) : (
<span className="loader" />
)}
<hr style={{ width: "100%" }} />
<div className="new-player-inputs">
<form className="new-player-inputs" onSubmit={handleSubmit}>
<div>
<label>name</label>
<input
type="text"
value={newPlayer.display_name}
required
value={player.display_name}
onChange={(e) =>
setNewPlayer({ ...newPlayer, display_name: e.target.value })
setPlayer({
...player,
display_name: e.target.value,
username: e.target.value.toLowerCase().replace(/\W/g, ""),
})
}
/>
</div>
@ -60,9 +98,10 @@ export const TeamPanel = () => {
<label>username</label>
<input
type="text"
value={newPlayer.username}
required
value={player.username}
onChange={(e) =>
setNewPlayer({ ...newPlayer, username: e.target.value })
setPlayer({ ...player, username: e.target.value })
}
/>
</div>
@ -70,16 +109,21 @@ export const TeamPanel = () => {
<label>number</label>
<input
type="text"
value={newPlayer.number}
value={player.number}
onChange={(e) =>
setNewPlayer({ ...newPlayer, number: e.target.value })
setPlayer({ ...player, number: e.target.value })
}
/>
</div>
</div>
<button className="team-player" id="new-team-player">
add player
</button>
<div>
{error && (
<span style={{ color: "red", margin: "auto" }}>{error}</span>
)}
</div>
<button className="team-player new-player">
{player.id === 0 ? "add player" : "modify player"}
</button>
</form>
</div>
</div>
);