Compare commits

...

3 Commits

Author SHA1 Message Date
453d7ca951
feat: loads of improvements (see comments)
1) check whether submitting user is submitting for himself
2) some refactoring of the tabs in `Ranking`
3) get chemistry and mvps from DB
4) restore previous
5) start over
6) (hopefully) improve logout
2025-03-13 20:11:34 +01:00
9afa4a88a8
feat: querySelector -> useRef 2025-03-13 15:01:22 +01:00
630986d49c
feat: rewrite of tabs component 2025-03-13 13:11:52 +01:00
5 changed files with 237 additions and 92 deletions

92
main.py
View File

@ -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):

View File

@ -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 {
@ -321,11 +323,20 @@ button,
opacity: 0.75;
}
.tablink {
color: white;
cursor: pointer;
.tab-button {
flex: 1;
background-color: grey;
border: none;
margin: 4px auto;
cursor: pointer;
opacity: 50%;
}
.tab-button.active {
opacity: unset;
font-weight: bold;
background-color: black;
color: white;
}
.navbar {

View File

@ -1,7 +1,8 @@
import { useEffect, 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<ReactSortableProps<any>> & {
orderedList?: boolean;
@ -21,15 +22,37 @@ function PlayerList(props: PlayerListProps) {
);
}
const LoadButton = (props: ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button {...props} style={{ padding: "4px 16px" }}>
🗃 restore previous
</button>
);
};
const ClearButton = (props: ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button {...props} style={{ padding: "4px 16px" }}>
🗑 start over
</button>
);
};
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<User[]>([]);
const [playersMiddle, setPlayersMiddle] = useState<User[]>(otherPlayers);
const [playersRight, setPlayersRight] = useState<User[]>([]);
@ -39,21 +62,36 @@ export function Chemistry({ user, players }: PlayerInfoProps) {
}, [players]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
async function handleSubmit() {
const dialog = document.querySelector("dialog[id='ChemistryDialog']");
(dialog as HTMLDialogElement).showModal();
if (dialogRef.current) dialogRef.current.showModal();
setDialog("sending...");
let left = playersLeft.map(({ id }) => id);
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 (
<>
<HeaderControl
onLoad={handleGet}
onClear={() => {
setPlayersRight([]);
setPlayersMiddle(otherPlayers);
setPlayersLeft([]);
}}
/>
<div className="container">
<div className="box three">
<h2>😬</h2>
@ -99,6 +137,7 @@ export function Chemistry({ user, players }: PlayerInfoProps) {
💾 <span className="submit_text">submit</span>
</button>
<dialog
ref={dialogRef}
id="ChemistryDialog"
onClick={(event) => {
event.currentTarget.close();
@ -110,28 +149,41 @@ export function Chemistry({ user, players }: PlayerInfoProps) {
);
}
export function MVP({ user, players }: PlayerInfoProps) {
function MVPDnD({ user, players }: PlayerInfoProps) {
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
const [rankedPlayers, setRankedPlayers] = useState<User[]>([]);
const [dialog, setDialog] = useState("dialog");
useEffect(() => {
setAvailablePlayers(players);
}, [players]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
async function handleSubmit() {
const dialog = document.querySelector("dialog[id='MVPDialog']");
(dialog as HTMLDialogElement).showModal();
if (dialogRef.current) dialogRef.current.showModal();
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 (
<>
<HeaderControl
onLoad={handleGet}
onClear={() => {
setAvailablePlayers(players);
setRankedPlayers([]);
}}
/>
<div className="container">
<div className="box two">
<h2>🥏🏃</h2>
@ -177,6 +229,7 @@ export function MVP({ user, players }: PlayerInfoProps) {
💾 <span className="submit_text">submit</span>
</button>
<dialog
ref={dialogRef}
id="MVPDialog"
onClick={(event) => {
event.currentTarget.close();
@ -188,9 +241,30 @@ export function MVP({ user, players }: PlayerInfoProps) {
);
}
interface HeaderControlProps {
onLoad: () => void;
onClear: () => void;
}
function HeaderControl({ onLoad, onClear }: HeaderControlProps) {
return (
<>
<div>
<LoadButton onClick={onLoad} />
<ClearButton onClick={onClear} />
</div>
<div>
<span className="grey">
assign as many or as few players as you want and don't forget to{" "}
<b>submit</b> 💾 when you're done :)
</span>
</div>
</>
);
}
export default function Rankings() {
const { user } = useSession();
const [players, setPlayers] = useState<User[]>([]);
const [players, setPlayers] = useState<User[] | null>(null);
const [openTab, setOpenTab] = useState("Chemistry");
async function loadPlayers() {
@ -206,66 +280,41 @@ export default function Rankings() {
loadPlayers();
}, []);
useEffect(() => {
openPage(openTab, "aliceblue");
}, [user]);
function openPage(pageName: string, color: string) {
// Hide all elements with class="tabcontent" by default */
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
(tabcontent[i] as HTMLElement).style.display = "none";
}
// Remove the background color of all tablinks/buttons
tablinks = document.getElementsByClassName("tablink");
for (i = 0; i < tablinks.length; i++) {
let button = tablinks[i] as HTMLElement;
button.style.opacity = "50%";
}
// Show the specific tab content
(document.getElementById(pageName) as HTMLElement).style.display = "block";
// Add the specific color to the button used to open the tab content
let activeButton = document.getElementById(
pageName + "Button"
) as HTMLElement;
activeButton.style.fontWeight = "bold";
activeButton.style.opacity = "100%";
document.body.style.backgroundColor = color;
setOpenTab(pageName);
}
const tabs = [
{ id: "Chemistry", label: "🧪 Chemistry" },
{ id: "MVP", label: "🏆 MVP" },
];
return (
<>
<div className="container navbar">
<button
className="tablink"
id="ChemistryButton"
onClick={() => openPage("Chemistry", "aliceblue")}
>
🧪 Chemistry
</button>
<button
className="tablink"
id="MVPButton"
onClick={() => openPage("MVP", "aliceblue")}
>
🏆 MVP
</button>
</div>
<span className="grey">
assign as many or as few players as you want
<br />
and don't forget to <b>submit</b> (💾) when you're done :)
</span>
<div id="Chemistry" className="tabcontent">
{user && <Chemistry {...{ user, players }} />}
</div>
<div id="MVP" className="tabcontent">
{user && <MVP {...{ user, players }} />}
</div>
{user && players && (
<div>
<div className="container navbar">
{tabs.map((tab) => (
<button
key={tab.id}
className={
openTab === tab.id ? "tab-button active" : "tab-button"
}
onClick={() => setOpenTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab) => {
if (openTab !== tab.id) return null;
switch (tab.id) {
case "Chemistry":
return <ChemistryDnD key={tab.id} {...{ user, players }} />;
case "MVP":
return <MVPDnD key={tab.id} {...{ user, players }} />;
default:
return null;
}
})}
</div>
)}
</>
);
}

View File

@ -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");
}
}

View File

@ -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[];
}