Compare commits

..

No commits in common. "453d7ca9518b1be9c7e4f0eb1e1f2046844869ad" and "4f30888c5c031a1012f0299ac1e024d7f03eeb9b" have entirely different histories.

5 changed files with 92 additions and 237 deletions

90
main.py
View File

@ -1,11 +1,9 @@
from typing import Annotated from fastapi import APIRouter, Depends, FastAPI, Security
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from db import Player, Team, Chemistry, MVPRanking, engine from db import Player, Team, Chemistry, MVPRanking, engine
from sqlmodel import ( from sqlmodel import (
Session, Session,
func,
select, select,
) )
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -20,9 +18,6 @@ from security import (
set_first_password, set_first_password,
) )
C = Chemistry
R = MVPRanking
P = Player
app = FastAPI(title="cutt") app = FastAPI(title="cutt")
api_router = APIRouter(prefix="/api") api_router = APIRouter(prefix="/api")
@ -85,83 +80,20 @@ team_router.add_api_route("/list", endpoint=list_teams, methods=["GET"])
team_router.add_api_route("/add", endpoint=add_team, methods=["POST"]) team_router.add_api_route("/add", endpoint=add_team, methods=["POST"])
wrong_user_id_exception = HTTPException( @api_router.post("/mvps", dependencies=[Depends(get_current_active_user)])
status_code=status.HTTP_401_UNAUTHORIZED, def submit_mvps(mvps: MVPRanking):
detail="you're not who you think you are...",
)
@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: with Session(engine) as session:
subquery = ( session.add(mvps)
select(R.user, func.max(R.time).label("latest")) session.commit()
.where(R.user == user.id) return JSONResponse("success!")
.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") @api_router.post("/chemistry", dependencies=[Depends(get_current_active_user)])
def submit_chemistry( def submit_chemistry(chemistry: 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: with Session(engine) as session:
subquery = ( session.add(chemistry)
select(C.user, func.max(C.time).label("latest")) session.commit()
.where(C.user == user.id) return JSONResponse("success!")
.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): class SPAStaticFiles(StaticFiles):

View File

@ -235,17 +235,15 @@ h3 {
margin: auto; margin: auto;
} }
button { button,
.button {
margin: 4px; margin: 4px;
font-weight: bold; font-weight: bold;
font-size: large;
color: aliceblue; color: aliceblue;
background-color: black; background-color: black;
border-radius: 1.2em; border-radius: 1.2em;
z-index: 1; z-index: 1;
&:hover {
opacity: 75%;
}
} }
#control-panel { #control-panel {
@ -323,20 +321,11 @@ button {
opacity: 0.75; opacity: 0.75;
} }
.tab-button { .tablink {
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; color: white;
cursor: pointer;
flex: 1;
margin: 4px auto;
} }
.navbar { .navbar {

View File

@ -1,8 +1,7 @@
import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { ReactSortable, ReactSortableProps } from "react-sortablejs"; import { ReactSortable, ReactSortableProps } from "react-sortablejs";
import { apiAuth, User } from "./api"; import { apiAuth, User } from "./api";
import { useSession } from "./Session"; import { useSession } from "./Session";
import { Chemistry, MVPRanking } from "./types";
type PlayerListProps = Partial<ReactSortableProps<any>> & { type PlayerListProps = Partial<ReactSortableProps<any>> & {
orderedList?: boolean; orderedList?: boolean;
@ -22,37 +21,15 @@ 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 { interface PlayerInfoProps {
user: User; user: User;
players: User[]; players: User[];
} }
function ChemistryDnD({ user, players }: PlayerInfoProps) { export function Chemistry({ user, players }: PlayerInfoProps) {
var otherPlayers = players.filter((player) => player.id !== user.id); const index = players.indexOf(user);
var otherPlayers = players.slice();
otherPlayers.splice(index, 1);
const [playersLeft, setPlayersLeft] = useState<User[]>([]); const [playersLeft, setPlayersLeft] = useState<User[]>([]);
const [playersMiddle, setPlayersMiddle] = useState<User[]>(otherPlayers); const [playersMiddle, setPlayersMiddle] = useState<User[]>(otherPlayers);
const [playersRight, setPlayersRight] = useState<User[]>([]); const [playersRight, setPlayersRight] = useState<User[]>([]);
@ -62,36 +39,21 @@ function ChemistryDnD({ user, players }: PlayerInfoProps) {
}, [players]); }, [players]);
const [dialog, setDialog] = useState("dialog"); const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
async function handleSubmit() { async function handleSubmit() {
if (dialogRef.current) dialogRef.current.showModal(); const dialog = document.querySelector("dialog[id='ChemistryDialog']");
(dialog as HTMLDialogElement).showModal();
setDialog("sending..."); setDialog("sending...");
let left = playersLeft.map(({ id }) => id); let left = playersLeft.map(({ id }) => id);
let middle = playersMiddle.map(({ id }) => id); let middle = playersMiddle.map(({ id }) => id);
let right = playersRight.map(({ id }) => id); let right = playersRight.map(({ id }) => id);
const data = { user: user.id, hate: left, undecided: middle, love: right }; const data = { user: user.id, hate: left, undecided: middle, love: right };
const response = await apiAuth("chemistry", data, "PUT"); const response = await apiAuth("chemistry", data, "POST");
setDialog(response || "try sending again"); response ? setDialog(response) : setDialog("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 ( return (
<> <>
<HeaderControl
onLoad={handleGet}
onClear={() => {
setPlayersRight([]);
setPlayersMiddle(otherPlayers);
setPlayersLeft([]);
}}
/>
<div className="container"> <div className="container">
<div className="box three"> <div className="box three">
<h2>😬</h2> <h2>😬</h2>
@ -137,7 +99,6 @@ function ChemistryDnD({ user, players }: PlayerInfoProps) {
💾 <span className="submit_text">submit</span> 💾 <span className="submit_text">submit</span>
</button> </button>
<dialog <dialog
ref={dialogRef}
id="ChemistryDialog" id="ChemistryDialog"
onClick={(event) => { onClick={(event) => {
event.currentTarget.close(); event.currentTarget.close();
@ -149,41 +110,28 @@ function ChemistryDnD({ user, players }: PlayerInfoProps) {
); );
} }
function MVPDnD({ user, players }: PlayerInfoProps) { export function MVP({ user, players }: PlayerInfoProps) {
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players); const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
const [rankedPlayers, setRankedPlayers] = useState<User[]>([]); const [rankedPlayers, setRankedPlayers] = useState<User[]>([]);
const [dialog, setDialog] = useState("dialog");
useEffect(() => { useEffect(() => {
setAvailablePlayers(players); setAvailablePlayers(players);
}, [players]); }, [players]);
const [dialog, setDialog] = useState("dialog");
const dialogRef = useRef<HTMLDialogElement>(null);
async function handleSubmit() { async function handleSubmit() {
if (dialogRef.current) dialogRef.current.showModal(); const dialog = document.querySelector("dialog[id='MVPDialog']");
(dialog as HTMLDialogElement).showModal();
setDialog("sending..."); setDialog("sending...");
let mvps = rankedPlayers.map(({ id }) => id); let mvps = rankedPlayers.map(({ id }) => id);
const data = { user: user.id, mvps: mvps }; const data = { user: user.id, mvps: mvps };
const response = await apiAuth("mvps", data, "PUT"); const response = await apiAuth("mvps", data, "POST");
response ? setDialog(response) : setDialog("try sending again"); 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 ( return (
<> <>
<HeaderControl
onLoad={handleGet}
onClear={() => {
setAvailablePlayers(players);
setRankedPlayers([]);
}}
/>
<div className="container"> <div className="container">
<div className="box two"> <div className="box two">
<h2>🥏🏃</h2> <h2>🥏🏃</h2>
@ -229,7 +177,6 @@ function MVPDnD({ user, players }: PlayerInfoProps) {
💾 <span className="submit_text">submit</span> 💾 <span className="submit_text">submit</span>
</button> </button>
<dialog <dialog
ref={dialogRef}
id="MVPDialog" id="MVPDialog"
onClick={(event) => { onClick={(event) => {
event.currentTarget.close(); event.currentTarget.close();
@ -241,30 +188,9 @@ function MVPDnD({ 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() { export default function Rankings() {
const { user } = useSession(); const { user } = useSession();
const [players, setPlayers] = useState<User[] | null>(null); const [players, setPlayers] = useState<User[]>([]);
const [openTab, setOpenTab] = useState("Chemistry"); const [openTab, setOpenTab] = useState("Chemistry");
async function loadPlayers() { async function loadPlayers() {
@ -280,41 +206,66 @@ export default function Rankings() {
loadPlayers(); loadPlayers();
}, []); }, []);
const tabs = [ useEffect(() => {
{ id: "Chemistry", label: "🧪 Chemistry" }, openPage(openTab, "aliceblue");
{ id: "MVP", label: "🏆 MVP" }, }, [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);
}
return ( return (
<> <>
{user && players && ( <div className="container navbar">
<div> <button
<div className="container navbar"> className="tablink"
{tabs.map((tab) => ( id="ChemistryButton"
<button onClick={() => openPage("Chemistry", "aliceblue")}
key={tab.id} >
className={ 🧪 Chemistry
openTab === tab.id ? "tab-button active" : "tab-button" </button>
} <button
onClick={() => setOpenTab(tab.id)} className="tablink"
> id="MVPButton"
{tab.label} onClick={() => openPage("MVP", "aliceblue")}
</button> >
))} 🏆 MVP
</div> </button>
{tabs.map((tab) => { </div>
if (openTab !== tab.id) return null;
switch (tab.id) { <span className="grey">
case "Chemistry": assign as many or as few players as you want
return <ChemistryDnD key={tab.id} {...{ user, players }} />; <br />
case "MVP": and don't forget to <b>submit</b> (💾) when you're done :)
return <MVPDnD key={tab.id} {...{ user, players }} />; </span>
default:
return null; <div id="Chemistry" className="tabcontent">
} {user && <Chemistry {...{ user, players }} />}
})} </div>
</div> <div id="MVP" className="tabcontent">
)} {user && <MVP {...{ user, players }} />}
</div>
</> </>
); );
} }

View File

@ -1,5 +1,3 @@
import { useSession } from "./Session";
export const baseUrl = import.meta.env.VITE_BASE_URL as string; export const baseUrl = import.meta.env.VITE_BASE_URL as string;
export async function apiAuth( export async function apiAuth(
@ -24,8 +22,7 @@ export async function apiAuth(
if (!resp.ok) { if (!resp.ok) {
if (resp.status === 401) { if (resp.status === 401) {
const { onLogout } = useSession(); logout();
onLogout();
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
} }

View File

@ -18,17 +18,3 @@ export interface PlayerRanking {
std: number; std: number;
n: 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[];
}