Compare commits

..

No commits in common. "8b092fed5152cfd994f5766d2dca1ea640b9a16f" and "bc6c2a4a98ce1009d8caf25339060cf21e0dc103" have entirely different histories.

10 changed files with 133 additions and 283 deletions

View File

@ -29,6 +29,7 @@ footer {
left: 8px; left: 8px;
} }
/*=========Network Controls=========*/ /*=========Network Controls=========*/
.infobutton { .infobutton {
@ -366,11 +367,7 @@ button,
position: relative; position: relative;
text-align: center; text-align: center;
height: 140px; height: 140px;
margin-bottom: 20px;
span {
display: block;
margin: 2px;
}
img { img {
display: block; display: block;
@ -393,15 +390,8 @@ button,
} }
} }
.avatar {
background-color: lightsteelblue;
padding: 2px 8px;
width: fit-content;
margin: auto;
}
.networkroute { .networkroute {
z-index: 3; z-index: 10;
position: absolute; position: absolute;
top: 24px; top: 24px;
left: 48px; left: 48px;

View File

@ -7,21 +7,34 @@ import { BrowserRouter, Routes, Route } from "react-router";
import { SessionProvider } from "./Session"; import { SessionProvider } from "./Session";
import { GraphComponent } from "./Network"; import { GraphComponent } from "./Network";
import MVPChart from "./MVPChart"; import MVPChart from "./MVPChart";
import Avatar from "./Avatar";
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<SessionProvider> <Header />
<Header /> <Routes>
<Routes> <Route index element={<Rankings />} />
<Route index element={<Rankings />} />
<Route path="/network" element={<GraphComponent />} /> <Route path="/network" element={
<Route path="/analysis" element={<Analysis />} /> <SessionProvider>
<Route path="/mvp" element={<MVPChart />} /> <GraphComponent />
</Routes> </SessionProvider>
<Footer /> } />
</SessionProvider>
<Route path="/analysis" element={
<SessionProvider>
<Analysis />
</SessionProvider>
} />
<Route path="/mvp" element={
<SessionProvider>
<MVPChart />
</SessionProvider>
} />
</Routes>
<Footer />
</BrowserRouter> </BrowserRouter>
); );
} }

View File

@ -1,99 +0,0 @@
import { MouseEventHandler, useEffect, useState } from "react";
import { useSession } from "./Session";
import { logout } from "./api";
interface ContextMenuItem {
label: string;
onClick: () => void;
}
export default function Avatar() {
const { user, onLogout } = useSession();
const [contextMenu, setContextMenu] = useState<{
open: boolean;
mouseX: number;
mouseY: number;
}>({ open: false, mouseX: 0, mouseY: 0 });
const contextMenuItems: ContextMenuItem[] = [
{ label: "View Profile", onClick: () => console.log("View Profile") },
{ label: "Edit Profile", onClick: () => console.log("Edit Profile") },
{ label: "Logout", onClick: onLogout },
];
const handleMenuClick: MouseEventHandler<HTMLDivElement> = (event) => {
event.preventDefault();
setContextMenu({
open: !contextMenu.open,
mouseX: event.clientX + 4,
mouseY: event.clientY + 2,
});
};
useEffect(() => {
if (contextMenu.open) {
document.addEventListener("click", handleCloseContextMenuOutside);
}
return () => {
document.removeEventListener("click", handleCloseContextMenuOutside);
};
}, [contextMenu.open]);
const handleMenuClose = () => {
setContextMenu({ ...contextMenu, open: false });
};
const handleCloseContextMenuOutside: MouseEventHandler<Document> = (
event
) => {
if (
!event.target ||
(!(event.target as Element).closest(".context-menu") &&
!(event.target as Element).closest(".avatar"))
) {
handleMenuClose();
}
};
return (
<div
className="avatar"
onContextMenu={handleMenuClick}
style={{ display: user ? "block" : "none" }}
onClick={handleMenuClick}
>
{user?.username}
{contextMenu.open && (
<ul
className="context-menu"
style={{
zIndex: 3,
position: "absolute",
top: contextMenu.mouseY,
left: contextMenu.mouseX,
background: "white",
border: "1px solid #ddd",
padding: 0,
margin: 0,
listStyle: "none",
}}
>
{contextMenuItems.map((item, index) => (
<li
key={index}
style={{
padding: "10px",
borderBottom: "1px solid #ddd",
cursor: "pointer",
}}
onClick={() => {
item.onClick();
handleMenuClose();
}}
>
{item.label}
</li>
))}
</ul>
)}
</div>
);
}

View File

@ -1,22 +1,20 @@
import { useLocation } from "react-router";
import { Link } from "react-router"; import { Link } from "react-router";
export default function Footer() { export default function Footer() {
const location = useLocation();
return ( return (
<footer className={location.pathname === "/network" ? "fixed-footer" : ""}> <footer>
<div className="navbar"> <div className="navbar">
<Link to="/"> <a href="/">
<span>Form</span> <span>Form</span>
</Link> </a>
<span>|</span> <span>|</span>
<Link to="/network"> <a href="/network">
<span>Trainer Analysis</span> <span>Trainer Analysis</span>
</Link> </a>
<span>|</span> <span>|</span>
<Link to="/mvp"> <a href="/mvp">
<span>MVP</span> <span>MVP</span>
</Link> </a>
</div> </div>
<p className="grey extra-margin"> <p className="grey extra-margin">
something not working? something not working?

View File

@ -1,18 +1,9 @@
import { Link, useLocation } from "react-router";
import Avatar from "./Avatar";
export default function Header() { export default function Header() {
const location = useLocation(); return <div className="logo" id="logo">
return ( <a href={"/"}>
<div className={location.pathname === "/network" ? "networkroute" : ""}> <img alt="logo" height="66%" src="logo.svg" />
<div className="logo"> <h3 className="centered">cutt</h3>
<Link to="/"> </a>
<img alt="logo" height="66%" src="logo.svg" /> <span className="grey">cool ultimate team tool</span>
<h3 className="centered">cutt</h3> </div>
</Link>
<span className="grey">cool ultimate team tool</span>
</div>
<Avatar />
</div>
);
} }

View File

@ -1,6 +1,6 @@
import { useState } from "react"; import { FormEvent, useContext, useState } from "react";
import { currentUser, login, User } from "./api"; import { useNavigate } from "react-router";
import Header from "./Header"; import { currentUser, login, LoginRequest, User } from "./api";
export interface LoginProps { export interface LoginProps {
onLogin: (user: User) => void; onLogin: (user: User) => void;
@ -15,58 +15,72 @@ export const Login = ({ onLogin }: LoginProps) => {
async function doLogin() { async function doLogin() {
setLoading(true); setLoading(true);
setError(null); setError(null);
const timeout = new Promise((r) => setTimeout(r, 1000)); const timeout = new Promise((r) => setTimeout(r, 1500));
let user: User; let user: User;
try { try {
await login({ username, password }); login({ username, password });
user = await currentUser(); user = await currentUser();
} catch (e) { } catch (e) {
await timeout; await timeout;
setError(e); setError(e);
setLoading(false); setLoading(false);
return; return
} }
await timeout; await timeout;
onLogin(user); onLogin(user);
} }
function handleClick() {
doLogin();
}
function handleSubmit(e: React.FormEvent) { function handleSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
doLogin(); doLogin();
} }
return ( return (
<> <form onSubmit={handleSubmit}>
<Header /> <div>
<form onSubmit={handleSubmit}> <input type="text" id="username" name="username" placeholder="username" required value={username} onChange={evt => setUsername(evt.target.value)} />
<div> </div>
<input <div>
type="text" <input type="password" id="password" name="password" placeholder="password" minLength={8} value={password} required onChange={evt => setPassword(evt.target.value)} />
id="username" </div>
name="username" <button type="submit" value="login" style={{ fontSize: "small" }} onClick={handleClick} >login</button>
placeholder="username" {loading && <span className="loader" />}
required </form>
value={username} )
onChange={(evt) => setUsername(evt.target.value)} }
/>
</div> /*
<div> export default function Login(props: { onLogin: (user: User) => void }) {
<input const { onLogin } = props;
type="password" const [username, setUsername] = useState("");
id="password" const [password, setPassword] = useState("");
name="password"
placeholder="password" async function handleLogin(e: FormEvent) {
minLength={8} e.preventDefault()
value={password} const timeout = new Promise((r) => setTimeout(r, 1500));
required let user: User;
onChange={(evt) => setPassword(evt.target.value)} try {
/> login({ username, password })
</div> user = await currentUser()
<button type="submit" value="login" style={{ fontSize: "small" }}> } catch (e) { await timeout; return }
login await timeout;
</button> onLogin(user);
{loading && <span className="loader" />} }
</form>
</> return <div>
); <form onSubmit={handleLogin}>
}; <div>
<input type="text" id="username" name="username" placeholder="username" required value={username} onChange={evt => setUsername(evt.target.value)} />
</div>
<div>
<input type="password" id="password" name="password" placeholder="password" minLength={8} value={password} required onChange={evt => setPassword(evt.target.value)} />
</div>
<input className="button" type="submit" value="login" onSubmit={handleLogin} />
</form>
</div>
} */

View File

@ -43,6 +43,15 @@ export const GraphComponent = () => {
const [popularity, setPopularity] = useState(false); const [popularity, setPopularity] = useState(false);
const [mutuality, setMutuality] = useState(false); const [mutuality, setMutuality] = useState(false);
const logo = document.getElementById("logo");
if (logo) {
logo.className = "logo networkroute";
}
const footer = document.getElementsByTagName("footer");
if (footer) {
(footer.item(0) as HTMLElement).className = "fixed-footer";
}
async function loadData() { async function loadData() {
setLoading(true); setLoading(true);
await apiAuth("analysis/graph_json", null) await apiAuth("analysis/graph_json", null)

View File

@ -1,6 +1,5 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { PlayerRanking } from "./types"; import { PlayerRanking } from "./types";
import { useSession } from "./Session";
interface RaceChartProps { interface RaceChartProps {
players: PlayerRanking[]; players: PlayerRanking[];
@ -35,13 +34,13 @@ const RaceChart: FC<RaceChartProps> = ({ players, std }) => {
const gap = 8; const gap = 8;
const maxValue = Math.max(...players.map((player) => player.rank)) + 1; const maxValue = Math.max(...players.map((player) => player.rank)) + 1;
const barHeight = (height - 2 * padding) / players.length; const barHeight = (height - 2 * padding) / players.length;
const fontSize = Math.min(barHeight - 1.5 * gap, width / 22); const fontSize = Math.min(barHeight - 1.5 * gap, width / 20);
return ( return (
<svg width={width} height={height} id="RaceChartSVG"> <svg width={width} height={height}>
{players.map((player, index) => ( {players.map((player, index) => (
<rect <rect
key={String(index)} key={index}
x={0} x={0}
y={index * barHeight + padding} y={index * barHeight + padding}
width={(1 - player.rank / maxValue) * width} width={(1 - player.rank / maxValue) * width}
@ -51,7 +50,7 @@ const RaceChart: FC<RaceChartProps> = ({ players, std }) => {
))} ))}
{players.map((player, index) => ( {players.map((player, index) => (
<g key={"group" + index}> <g>
<text <text
key={index + "_name"} key={index + "_name"}
x={4} x={4}

View File

@ -1,94 +1,36 @@
import { import { createContext, ReactNode, useContext, useLayoutEffect, useState } from "react";
createContext, import { currentUser, User } from "./api";
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import { currentUser, logout, User } from "./api";
import { Login } from "./Login"; import { Login } from "./Login";
import Header from "./Header";
export interface SessionProviderProps { export interface SessionProviderProps {
children: ReactNode; children: ReactNode;
} }
export interface Session { const sessionContext = createContext<User | null>(null);
user: User | null;
onLogout: () => void;
}
const sessionContext = createContext<Session>({
user: null,
onLogout: () => {},
});
export function SessionProvider(props: SessionProviderProps) { export function SessionProvider(props: SessionProviderProps) {
const { children } = props; const { children } = props;
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [err, setErr] = useState<unknown>(null); const [err, setErr] = useState<unknown>(null);
const [loading, setLoading] = useState(false);
function loadUser() { function loadUser() {
setLoading(true);
currentUser() currentUser()
.then((user) => { .then((user) => { setUser(user); setErr(null); })
setUser(user); .catch((err) => { setUser(null); setErr(err); });
setErr(null);
})
.catch((err) => {
setUser(null);
setErr(err);
})
.finally(() => setLoading(false));
} }
useEffect(() => { useLayoutEffect(() => { loadUser(); }, [err]);
loadUser();
}, []);
function onLogin(user: User) { function onLogin(user: User) {
setUser(user); setUser(user);
setErr(null); setErr(null);
} }
async function onLogout() {
try {
logout();
setUser(null);
setErr({ message: "Logged out successfully" });
console.log("logged out.");
setLoading(true); // Set loading to true
loadUser();
} catch (e) {
console.error(e);
setErr(e); // Update the error state if logout fails
}
}
console.log("sanity", user);
let content: ReactNode; let content: ReactNode;
if (loading || (!err && !user)) if (!err && !user) content = <span className="loader" />;
content = ( else if (err) content = <Login onLogin={onLogin} />;
<> else content = <sessionContext.Provider value={user}>{children}</sessionContext.Provider>;
<Header />
<span className="loader" />
</>
);
else if (err) {
if ((err as any).message === "Logged out successfully") {
setTimeout(() => setErr(null), 1000);
content = <Login onLogin={onLogin} />;
} else {
content = <Login onLogin={onLogin} />;
}
} else
content = (
<sessionContext.Provider value={{ user, onLogout }}>
{children}
</sessionContext.Provider>
);
return content; return content;
} }

View File

@ -88,29 +88,22 @@ export type Token = {
token_type: string; token_type: string;
}; };
// api.js export const login = (req: LoginRequest) => {
export const login = async (req: LoginRequest): Promise<void> => { fetch(`${baseUrl}api/token`, {
try { method: "POST",
const response = await fetch(`${baseUrl}api/token`, { headers: {
method: "POST", "Content-Type": "application/x-www-form-urlencoded",
headers: { },
"Content-Type": "application/x-www-form-urlencoded", body: new URLSearchParams(req).toString(),
}, })
body: new URLSearchParams(req).toString(), .then((resp) => resp.json() as Promise<Token>)
}); .then((token) =>
if (!response.ok) { token
throw new Error(`HTTP error! status: ${response.status}`); ? localStorage.setItem("access_token", token.access_token)
} : console.log("token not acquired")
const token = (await response.json()) as Token; )
if (token && token.access_token) { .catch((e) => console.log("catch error " + e + " in login"));
localStorage.setItem("access_token", token.access_token); return Promise<void>;
} else {
console.log("Token not acquired");
}
} catch (e) {
console.error(e);
throw e; // rethrow the error so it can be caught by the caller
}
}; };
export const logout = () => localStorage.removeItem("access_token"); export const logout = () => localStorage.removeItem("access_token");