Compare commits
3 Commits
4f30888c5c
...
453d7ca951
Author | SHA1 | Date | |
---|---|---|---|
453d7ca951 | |||
9afa4a88a8 | |||
630986d49c |
92
main.py
92
main.py
@ -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):
|
||||
|
23
src/App.css
23
src/App.css
@ -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 {
|
||||
|
195
src/Rankings.tsx
195
src/Rankings.tsx
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
14
src/types.ts
14
src/types.ts
@ -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[];
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user