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 typing import Annotated
from fastapi import APIRouter, Depends, Security from fastapi import APIRouter, Depends, HTTPException, Security, status
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from pydantic import BaseModel
from sqlmodel import Session, select from sqlmodel import Session, select
from cutt.db import Player, Team, engine from cutt.db import Player, PlayerTeamLink, Team, engine
from cutt.security import change_password, get_current_active_user, read_player_me from cutt.security import (
TeamScopedRequest,
change_password,
get_current_active_user,
read_player_me,
verify_team_scope,
)
P = Player P = Player
player_router = APIRouter(prefix="/player", tags=["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: 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() session.commit()
@ -48,8 +90,9 @@ async def list_players(team_id: int):
players = [t.players for t in session.exec(statement)][0] players = [t.players for t in session.exec(statement)][0]
if players: if players:
return [ return [
player.model_dump(include={"id", "display_name", "number"}) player.model_dump(include={"id", "display_name", "username", "number"})
for player in players 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( player_router.add_api_route(
"/add", "/add/{team_id}",
endpoint=add_player, endpoint=add_player,
methods=["POST"], methods=["POST"],
dependencies=[Security(get_current_active_user, scopes=["admin"])],
) )
player_router.add_api_route( player_router.add_api_route(
"/list/{team_id}", "/list/{team_id}",

View File

@ -480,6 +480,7 @@ button {
/*========TEAM PANEL========*/ /*========TEAM PANEL========*/
.team-panel { .team-panel {
max-width: 800px;
padding: 1em; padding: 1em;
border: 3px solid black; border: 3px solid black;
box-shadow: 8px 8px black; box-shadow: 8px 8px black;
@ -492,44 +493,47 @@ button {
} }
.team-player { .team-player {
cursor: pointer;
color: black; color: black;
background-color: #36c4; background-color: #36c4;
border: 1px solid black; border: 1px solid black;
border-radius: 1em; border-radius: 1.4em;
margin: 4px; margin: 4px;
padding: 0 0.5em; padding: 0.2em 0.5em;
&:hover { &:hover {
background-color: #36c8; background-color: #36c8;
} }
&.new-player {
background-color: #3838;
}
} }
.new-player-inputs { .new-player-inputs {
margin: auto; margin: auto;
div { div {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: 20ch auto;
margin: auto;
@media only screen and (max-width: 768px) {
grid-template-columns: auto;
place-items: center;
}
label { label {
text-align: right; text-align: left;
margin-left: 2em; width: 20ch;
margin-right: auto; margin: auto 1em;
} }
input { input {
width: 200px; width: 90%;
margin: 2px 1em; margin: 4px 0;
} }
} }
} }
#new-team-player {
background-color: #3838;
font-weight: bold;
}
@keyframes blink { @keyframes blink {

View File

@ -114,7 +114,7 @@ export const Login = ({ onLogin }: LoginProps) => {
{visible ? <Eye /> : <EyeSlash />} {visible ? <Eye /> : <EyeSlash />}
</div> </div>
</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" }}> <button type="submit" value="login" style={{ fontSize: "small" }}>
login login
</button> </button>

View File

@ -1,21 +1,38 @@
import { useEffect, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import { loadPlayers, User } from "./api"; import { apiAuth, loadPlayers, User } from "./api";
import { useSession } from "./Session"; import { useSession } from "./Session";
export const TeamPanel = () => { export const TeamPanel = () => {
const { teams } = useSession(); const { teams } = useSession();
const [players, setPlayers] = useState<User[] | null>(null); const newPlayerTemplate = {
const [newPlayer, setNewPlayer] = useState({ id: 0,
username: "", username: "",
display_name: "", display_name: "",
number: "", number: "",
}); email: "",
} as User;
const [error, setError] = useState("");
const [players, setPlayers] = useState<User[] | null>(null);
const [player, setPlayer] = useState(newPlayerTemplate);
useEffect(() => { useEffect(() => {
if (teams) { if (teams) {
loadPlayers(teams.activeTeam).then((data) => setPlayers(data)); loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
} }
}, [teams]); }, [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) { if (teams) {
const activeTeam = teams.teams.filter( const activeTeam = teams.teams.filter(
(team) => team.id == teams?.activeTeam (team) => team.id == teams?.activeTeam
@ -23,36 +40,57 @@ export const TeamPanel = () => {
return ( return (
<div className="team-panel"> <div className="team-panel">
<h1>{activeTeam.name}</h1> <h1>{activeTeam.name}</h1>
<div className="stack column"> <div>
<input type="text" value={activeTeam.location || ""} disabled /> <input type="text" value={activeTeam.location || ""} disabled />
<br />
<input type="text" value={activeTeam.country || ""} disabled /> <input type="text" value={activeTeam.country || ""} disabled />
<hr style={{ width: "100%" }} /> <hr style={{ width: "100%" }} />
<h2>players</h2> <h2>players</h2>
<div {players ? (
style={{ <div
display: "flex", style={{
flexWrap: "wrap", display: "flex",
justifyContent: "center", flexWrap: "wrap",
}} justifyContent: "center",
> }}
{players && >
players.map((player) => ( {players &&
<div className="team-player" key={player.id}> players.map((p) => (
{player.display_name} <button
</div> className="team-player"
))} key={p.id}
</div> 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%" }} /> <hr style={{ width: "100%" }} />
<div className="new-player-inputs">
<form className="new-player-inputs" onSubmit={handleSubmit}>
<div> <div>
<label>name</label> <label>name</label>
<input <input
type="text" type="text"
value={newPlayer.display_name} required
value={player.display_name}
onChange={(e) => 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> </div>
@ -60,9 +98,10 @@ export const TeamPanel = () => {
<label>username</label> <label>username</label>
<input <input
type="text" type="text"
value={newPlayer.username} required
value={player.username}
onChange={(e) => onChange={(e) =>
setNewPlayer({ ...newPlayer, username: e.target.value }) setPlayer({ ...player, username: e.target.value })
} }
/> />
</div> </div>
@ -70,16 +109,21 @@ export const TeamPanel = () => {
<label>number</label> <label>number</label>
<input <input
type="text" type="text"
value={newPlayer.number} value={player.number}
onChange={(e) => onChange={(e) =>
setNewPlayer({ ...newPlayer, number: e.target.value }) setPlayer({ ...player, number: e.target.value })
} }
/> />
</div> </div>
</div> <div>
<button className="team-player" id="new-team-player"> {error && (
add player <span style={{ color: "red", margin: "auto" }}>{error}</span>
</button> )}
</div>
<button className="team-player new-player">
{player.id === 0 ? "add player" : "modify player"}
</button>
</form>
</div> </div>
</div> </div>
); );