diff --git a/web/src/components/HomePage/HeatMapSmall.tsx b/web/src/components/HomePage/HeatMapSmall.tsx index 40f533da..30e8fc95 100644 --- a/web/src/components/HomePage/HeatMapSmall.tsx +++ b/web/src/components/HomePage/HeatMapSmall.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Box, Flex, Grid } from "@chakra-ui/react"; import { valueToColor } from "../../utils/colors"; -const HeatMapSmall = ({ rates, size }: { rates: number[]; size?: string }) => { +const HeatMapSmall = ({ rates, size }: { rates: number[] | undefined; size?: string }) => { const generateCell = (index: number) => ( { alignItems="center" justifyContent="center" cursor={"pointer"} - bg={valueToColor(rates[index], rates)} + bg={rates ? valueToColor(rates[index], rates) : valueToColor(0, [0])} /> ); diff --git a/web/src/components/atbat/AtBatFooter.module.css b/web/src/components/atbat/AtBatFooter.module.css index 771d853f..e44a5614 100644 --- a/web/src/components/atbat/AtBatFooter.module.css +++ b/web/src/components/atbat/AtBatFooter.module.css @@ -9,6 +9,7 @@ background: #FCECD9; z-index: 2; margin-top: 15px; + position: relative; } .vs { diff --git a/web/src/components/atbat/AtBatFooter.tsx b/web/src/components/atbat/AtBatFooter.tsx index 88bce655..2793d1df 100644 --- a/web/src/components/atbat/AtBatFooter.tsx +++ b/web/src/components/atbat/AtBatFooter.tsx @@ -1,19 +1,60 @@ import styles from "./AtBatFooter.module.css"; -import { AtBatStatus } from "../../types"; -import TokenToPlay from "../HomePage/TokenToPlay"; +import { AtBatStatus, Token } from "../../types"; import TokenCardSmall from "./TokenCardSmall"; +import { useEffect, useRef, useState } from "react"; +import TokenCard from "./TokenCard"; +import { sendReport } from "../../utils/humbug"; const AtBatFooter = ({ atBat }: { atBat: AtBatStatus }) => { + const [showDetailsFor, setShowDetailsFor] = useState(undefined); + const tokenCardRef = useRef(null); + const handleClickOutside = (event: MouseEvent) => { + if (tokenCardRef.current && !tokenCardRef.current.contains(event.target as Node)) { + setShowDetailsFor((prev) => { + const opener = document.getElementById(`token-card-small-${prev?.address}-${prev?.id}`); + if (opener?.contains(event.target as Node)) { + event.stopPropagation(); //preventing handleClick that reopens details for same token + } + return undefined; + }); + } + }; + useEffect(() => { + document.addEventListener("click", handleClickOutside, true); + return () => document.removeEventListener("click", handleClickOutside, true); + }, []); + const handleClick = (token: Token | undefined) => { + sendReport("Details opened", {}, ["type:click", "click:open_details"]); + setShowDetailsFor(token); + }; + return (
+ {showDetailsFor && ( + + )} {atBat.pitcher ? ( - + handleClick(atBat.pitcher)} + /> ) : (
)}
VS
{atBat.batter ? ( - + handleClick(atBat.batter)} + /> ) : (
)} diff --git a/web/src/components/atbat/TokenCard.module.css b/web/src/components/atbat/TokenCard.module.css index fbde147c..4c8daae7 100644 --- a/web/src/components/atbat/TokenCard.module.css +++ b/web/src/components/atbat/TokenCard.module.css @@ -20,6 +20,12 @@ transform: translateX(-65%); } +.imageAndInfo { + display: flex; + gap: 10px; + flex-direction: column; +} + .image { width: 260px; min-width: 260px; @@ -59,4 +65,41 @@ align-items: flex-start; gap: 10px; align-self: stretch; +} + +@media (max-width: 1023px) { + .pitcherContainer, .batterContainer { + position: absolute; + bottom: 80px; + left: 50%; + transform: translateX(-50%); + height: 364px; + min-height: 342px; + + padding: 10px; + width: 300px; + gap: 10px; + } + + .imageAndInfo { + flex-direction: row; + align-items: flex-start; + } + + .image { + width: 80px; + min-width: 80px; + height: 80px; + } + + .tokenName { + font-size: 18px; + line-height: 100%; + } + + .icon { + display: none; + } + + } \ No newline at end of file diff --git a/web/src/components/atbat/TokenCard.tsx b/web/src/components/atbat/TokenCard.tsx index 85414330..807ed498 100644 --- a/web/src/components/atbat/TokenCard.tsx +++ b/web/src/components/atbat/TokenCard.tsx @@ -1,30 +1,29 @@ +import React from "react"; import styles from "./TokenCard.module.css"; import { Image } from "@chakra-ui/react"; -import { PitchLocation, SwingLocation, Token } from "../../types"; +import { Token } from "../../types"; import BatIconBig from "../icons/BatIconBig"; import BallIconBig from "../icons/BallIconBig"; import { useQuery } from "react-query"; -import axios from "axios"; import MainStat from "../playing/MainStat"; import HeatMap from "../playing/HeatMap"; import DetailedStat from "../playing/DetailedStat"; +import { + fetchBatterStats, + fetchPitchDistribution, + fetchPitcherStats, + fetchSwingDistribution, +} from "../../utils/stats"; -const TokenCard = ({ token, isPitcher }: { token: Token; isPitcher: boolean }) => { +interface TokenCardProps extends React.RefAttributes { + token: Token; + isPitcher: boolean; +} + +const TokenCard: React.FC = React.forwardRef(({ token, isPitcher }, ref) => { const pitcherStats = useQuery( - ["pitcher_stat", token.id], - async () => { - if (!token) { - return; - } - const API_URL = "https://api.fullcount.xyz/stats"; - try { - const stat = await axios.get(`${API_URL}/${token.address}/${token.id}`); - return stat.data; - } catch (e) { - console.log({ token, e }); - return 0; - } - }, + ["pitcher_stat", token?.address, token?.id], + () => fetchPitcherStats(token), { enabled: !!token && isPitcher, retryDelay: (attemptIndex) => (attemptIndex < 1 ? 5000 : 10000), @@ -36,20 +35,8 @@ const TokenCard = ({ token, isPitcher }: { token: Token; isPitcher: boolean }) = ); const batterStats = useQuery( - ["batter_stat", token.id], - async () => { - if (!token) { - return; - } - const API_URL = "https://api.fullcount.xyz/stats"; - try { - const stat = await axios.get(`${API_URL}/${token.address}/${token.id}`); - return stat.data; - } catch (e) { - console.log({ token, e }); - return; - } - }, + ["batter_stat", token?.address, token?.id], + () => fetchBatterStats(token), { enabled: !!token && !isPitcher, retryDelay: (attemptIndex) => (attemptIndex < 1 ? 5000 : 10000), @@ -61,32 +48,8 @@ const TokenCard = ({ token, isPitcher }: { token: Token; isPitcher: boolean }) = ); const pitchDistributions = useQuery( - ["pitch_distribution", token.id], - async () => { - if (!token) { - return; - } - const API_URL = "https://api.fullcount.xyz/pitch_distribution"; - const counts = new Array(25).fill(0); - try { - const res = await axios.get(`${API_URL}/${token.address}/${token.id}`); - res.data.pitch_distribution.forEach((l: PitchLocation) => { - counts[l.pitch_vertical * 5 + l.pitch_horizontal] = - counts[l.pitch_vertical * 5 + l.pitch_horizontal] + l.count; - }); - const total = counts.reduce((acc, value) => acc + value); - const fast = res.data.pitch_distribution.reduce( - (acc: number, value: { pitch_speed: 0 | 1; count: number }) => - acc + (value.pitch_speed === 0 ? value.count : 0), - 0, - ); - const rates = counts.map((value) => value / total); - return { rates, counts, fast }; - } catch (e) { - console.log({ token, e }); - return { counts, rates: counts, fast: 0 }; - } - }, + ["pitch_distribution", token?.address, token?.id], + () => fetchPitchDistribution(token), { enabled: !!token && isPitcher, retryDelay: (attemptIndex) => (attemptIndex < 1 ? 5000 : 10000), @@ -98,29 +61,8 @@ const TokenCard = ({ token, isPitcher }: { token: Token; isPitcher: boolean }) = ); const swingDistributions = useQuery( - ["swing_distribution", token.id], - async () => { - if (!token) { - return; - } - const API_URL = "https://api.fullcount.xyz/swing_distribution"; - const counts = new Array(25).fill(0); - try { - const res = await axios.get(`${API_URL}/${token.address}/${token.id}`); - let takes = 0; - res.data.swing_distribution.forEach((l: SwingLocation) => - l.swing_type === 2 - ? (takes += l.count) - : (counts[l.swing_vertical * 5 + l.swing_horizontal] = l.count), - ); - const total = counts.reduce((acc, value) => acc + value); - const rates = counts.map((value) => value / total); - return { rates, counts, takes }; - } catch (e) { - console.log({ token, e }); - return { counts, rates: counts, takes: 0 }; - } - }, + ["swing_distribution", token?.address, token?.id], + () => fetchSwingDistribution(token), { enabled: !!token && !isPitcher, retryDelay: (attemptIndex) => (attemptIndex < 1 ? 5000 : 10000), @@ -130,17 +72,20 @@ const TokenCard = ({ token, isPitcher }: { token: Token; isPitcher: boolean }) = refetchInterval: 50000, }, ); + return ( -
- {""} -
- {!isPitcher ? ( - - ) : ( - - )} -
{token.name}
-
{token.id}
+
+
+ {""} +
+ {!isPitcher ? ( + + ) : ( + + )} +
{token.name}
+
{token.id}
+
@@ -171,6 +116,8 @@ const TokenCard = ({ token, isPitcher }: { token: Token; isPitcher: boolean }) =
); -}; +}); + +TokenCard.displayName = "TokenCard"; export default TokenCard; diff --git a/web/src/components/atbat/TokenCardSmall.tsx b/web/src/components/atbat/TokenCardSmall.tsx index c95c4de9..649ca7ff 100644 --- a/web/src/components/atbat/TokenCardSmall.tsx +++ b/web/src/components/atbat/TokenCardSmall.tsx @@ -1,10 +1,10 @@ -import { PitchLocation, SwingLocation, Token } from "../../types"; +import { Token } from "../../types"; import styles from "../HomePage/TokenToPlay.module.css"; import Image from "next/image"; import { useQuery } from "react-query"; -import axios from "axios"; import HeatMapSmall from "../HomePage/HeatMapSmall"; import { Spinner } from "@chakra-ui/react"; +import { fetchPitchDistribution, fetchSwingDistribution } from "../../utils/stats"; const TokenCardSmall = ({ token, @@ -23,56 +23,27 @@ const TokenCardSmall = ({ }) => { const pitchDistributions = useQuery( ["pitch_distribution", token?.address, token?.id], - async () => { - if (!token || !isPitcher) { - return; - } - const API_URL = "https://api.fullcount.xyz/pitch_distribution"; - try { - const res = await axios.get(`${API_URL}/${token.address}/${token.id}`); - const counts = new Array(25).fill(0); - res.data.pitch_distribution.forEach( - (l: PitchLocation) => (counts[l.pitch_vertical * 5 + l.pitch_horizontal] = l.count), - ); - const total = counts.reduce((acc, value) => acc + value); - const rates = counts.map((value) => value / total); - return { rates, counts }; - } catch (e) { - console.log({ token, e }); - return; - } - }, + () => fetchPitchDistribution(token), { enabled: !!token && isPitcher, + retryDelay: (attemptIndex) => (attemptIndex < 1 ? 5000 : 10000), + retry: (failureCount) => { + return failureCount < 3; + }, + refetchInterval: 50000, }, ); const swingDistributions = useQuery( ["swing_distribution", token?.address, token?.id], - async () => { - if (!token || isPitcher) { - return; - } - const API_URL = "https://api.fullcount.xyz/swing_distribution"; - try { - const res = await axios.get(`${API_URL}/${token.address}/${token.id}`); - const counts = new Array(25).fill(0); - let takes = 0; - res.data.swing_distribution.forEach((l: SwingLocation) => - l.swing_type === 2 - ? (takes += l.count) - : (counts[l.swing_vertical * 5 + l.swing_horizontal] = l.count), - ); - const total = counts.reduce((acc, value) => acc + value); - const rates = counts.map((value) => value / total); - return { rates, counts, takes }; - } catch (e) { - console.log({ token, e }); - return; - } - }, + () => fetchSwingDistribution(token), { enabled: !!token && !isPitcher, + retryDelay: (attemptIndex) => (attemptIndex < 1 ? 5000 : 10000), + retry: (failureCount) => { + return failureCount < 3; + }, + refetchInterval: 50000, }, ); if (!token) { @@ -81,16 +52,20 @@ const TokenCardSmall = ({ if (isForGame) { return ( -
+
{""} - {isPitcher && pitchDistributions.data && ( + {isPitcher && (
- +
)} - {!isPitcher && swingDistributions.data && ( + {!isPitcher && (
- +
)}
diff --git a/web/src/components/playing/MainStat.module.css b/web/src/components/playing/MainStat.module.css index 39dfc63d..2273abe0 100644 --- a/web/src/components/playing/MainStat.module.css +++ b/web/src/components/playing/MainStat.module.css @@ -1,16 +1,16 @@ .container, .emptyStatContainer { display: flex; - padding: 10px 5px; + padding: 5px 10px; justify-content: center; gap: 10px; border: 0.5px solid #262019; background: #FFF; - align-items: baseline; + align-items: flex-end; } .emptyStatContainer { justify-content: space-between; - padding: 10px 15px; + padding: 5px 15px; } .divider { @@ -54,7 +54,6 @@ } .label { font-size: 6px; - margin-left: 0px; - + margin-left: 0; } } diff --git a/web/src/utils/stats.ts b/web/src/utils/stats.ts new file mode 100644 index 00000000..b946a4f8 --- /dev/null +++ b/web/src/utils/stats.ts @@ -0,0 +1,86 @@ +import { PitchLocation, SwingLocation, Token } from "../types"; +import axios from "axios"; + +export const fetchSwingDistribution = async (token: Token | undefined) => { + if (!token) { + return; + } + + const API_URL = "https://api.fullcount.xyz/swing_distribution"; + const counts = new Array(25).fill(0); + try { + const res = await axios.get(`${API_URL}/${token.address}/${token.id}`); + let takes = 0; + if (!res.data.swing_distribution) { + return { counts, rates: counts, takes: 0 }; + } + res.data.swing_distribution.forEach((l: SwingLocation) => { + if (l.swing_type === 2) { + takes += l.count; + } else { + counts[l.swing_vertical * 5 + l.swing_horizontal] += l.count; + } + }); + const total = counts.reduce((acc, value) => acc + value, 0); + const rates = counts.map((value) => value / total); + return { rates, counts, takes }; + } catch (e) { + console.error("Error fetching swing distribution:", e); + return { counts, rates: counts, takes: 0 }; + } +}; + +export const fetchPitchDistribution = async (token: Token | undefined) => { + if (!token) { + return; + } + const API_URL = "https://api.fullcount.xyz/pitch_distribution"; + const counts = new Array(25).fill(0); + try { + const res = await axios.get(`${API_URL}/${token.address}/${token.id}`); + if (!res.data.pitch_distribution) { + return { counts, rates: counts, fast: 0 }; + } + res.data.pitch_distribution.forEach((l: PitchLocation) => { + counts[l.pitch_vertical * 5 + l.pitch_horizontal] = + counts[l.pitch_vertical * 5 + l.pitch_horizontal] + l.count; + }); + const total = counts.reduce((acc, value) => acc + value); + const fast = res.data.pitch_distribution.reduce( + (acc: number, value: { pitch_speed: 0 | 1; count: number }) => + acc + (value.pitch_speed === 0 ? value.count : 0), + 0, + ); + const rates = counts.map((value) => value / total); + return { rates, counts, fast }; + } catch (e) { + console.log({ token, e }); + return { counts, rates: counts, fast: 0 }; + } +}; + +export const fetchBatterStats = async (token: Token) => { + if (!token) { + return; + } + const API_URL = "https://api.fullcount.xyz/stats"; + try { + const stat = await axios.get(`${API_URL}/${token.address}/${token.id}`); + return stat.data; + } catch (e) { + console.log({ token, e }); + } +}; + +export const fetchPitcherStats = async (token: Token) => { + if (!token) { + return; + } + const API_URL = "https://api.fullcount.xyz/stats"; + try { + const stat = await axios.get(`${API_URL}/${token.address}/${token.id}`); + return stat.data; + } catch (e) { + console.log({ token, e }); + } +};