Skip to content

Commit 7b615f4

Browse files
feat(web): extra statistic on homepage
1 parent 5f3d237 commit 7b615f4

File tree

9 files changed

+328
-1
lines changed

9 files changed

+328
-1
lines changed
Loading
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { StyledSkeleton } from "components/StyledSkeleton";
5+
6+
const Container = styled.div`
7+
display: flex;
8+
max-width: 380px;
9+
align-items: center;
10+
gap: 8px;
11+
`;
12+
13+
const SVGContainer = styled.div`
14+
height: 14px;
15+
width: 14px;
16+
display: flex;
17+
align-items: center;
18+
justify-content: center;
19+
svg {
20+
fill: ${({ theme }) => theme.secondaryPurple};
21+
}
22+
`;
23+
24+
const TextContainer = styled.div`
25+
display: flex;
26+
align-items: center;
27+
gap: 4px;
28+
p {
29+
min-width: 60px;
30+
}
31+
`;
32+
33+
export interface IExtraStatsDisplay {
34+
title: string;
35+
text: string;
36+
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
37+
}
38+
39+
const ExtraStatsDisplay: React.FC<IExtraStatsDisplay> = ({ title, text, icon: Icon, ...props }) => {
40+
return (
41+
<Container {...props}>
42+
<SVGContainer>{<Icon />}</SVGContainer>
43+
<TextContainer>
44+
<label>{title}:</label>
45+
<p>{text !== null ? text : <StyledSkeleton />}</p>
46+
</TextContainer>
47+
</Container>
48+
);
49+
};
50+
51+
export default ExtraStatsDisplay;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
3+
import { useGraphqlBatcher } from "context/GraphqlBatcher";
4+
5+
import { graphql } from "src/graphql";
6+
import { HomePageBlockQuery } from "src/graphql/graphql";
7+
export type { HomePageBlockQuery };
8+
9+
const homePageBlockQuery = graphql(`
10+
query HomePageBlock($blockNumber: Int) {
11+
courts(orderBy: id, orderDirection: asc, block: { number: $blockNumber }) {
12+
id
13+
name
14+
numberDisputes
15+
feeForJuror
16+
stake
17+
}
18+
}
19+
`);
20+
21+
export const useHomePageBlockQuery = (blockNumber: number) => {
22+
const isEnabled = blockNumber != null;
23+
const { graphqlBatcher } = useGraphqlBatcher();
24+
25+
return useQuery({
26+
queryKey: [`homePageBlockQuery${blockNumber}`],
27+
enabled: isEnabled,
28+
queryFn: async () => {
29+
const data = await graphqlBatcher.fetch({
30+
id: crypto.randomUUID(),
31+
document: homePageBlockQuery,
32+
variables: { blockNumber },
33+
});
34+
return data;
35+
},
36+
});
37+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useMemo } from "react";
2+
3+
import { DEFAULT_CHAIN } from "consts/chains";
4+
import { getOneWeekAgoTimestamp } from "utils/date";
5+
6+
import { HomePageBlockQuery } from "src/graphql/graphql";
7+
import { isUndefined } from "src/utils";
8+
9+
import { useBlockByTimestamp } from "../useBlockByTimestamp";
10+
import { useHomePageContext } from "../useHomePageContext";
11+
12+
import { useHomePageBlockQuery } from "./useHomePageBlockQuery";
13+
14+
type Court = HomePageBlockQuery["courts"][number];
15+
16+
const getCourtWithMaxDifference = (initialCourts: Court[], endCourts: Court[]): Court => {
17+
const diffs = initialCourts.map((court, idx) => {
18+
return Number(endCourts[idx].numberDisputes) - Number(court.numberDisputes);
19+
});
20+
21+
const maxDiffCourtId = diffs.reduce((a, b) => (a > b ? a : b));
22+
23+
return initialCourts[diffs.indexOf(maxDiffCourtId)];
24+
};
25+
26+
const getCourtWithMaxReward = (courts: Court[]): Court => {
27+
return courts.reduce((a, b) => (Number(a.feeForJuror) > Number(b.feeForJuror) ? a : b));
28+
};
29+
30+
const getCourtWithMaxChance = (courts: Court[]): Court => {
31+
return courts.reduce((a, b) => (Number(a.stake) > Number(b.feeForJuror) ? b : a));
32+
};
33+
34+
interface HomePageExtraStats {
35+
MostActiveCourt: string | null | undefined;
36+
HighestDrawingChance: string | null | undefined;
37+
HighestRewardChance: string | null | undefined;
38+
}
39+
40+
export const useHomePageExtraStats = (): HomePageExtraStats => {
41+
const { data } = useHomePageContext();
42+
const { blockNumber } = useBlockByTimestamp(
43+
DEFAULT_CHAIN,
44+
useMemo(() => getOneWeekAgoTimestamp(), [])
45+
);
46+
47+
const { data: relData } = useHomePageBlockQuery(blockNumber!);
48+
49+
const HighestDrawingChance = useMemo(() => {
50+
return data ? getCourtWithMaxChance(data.courts).name : null;
51+
}, [data]);
52+
53+
const HighestRewardChance = useMemo(() => {
54+
return data ? getCourtWithMaxReward(data.courts).name : null;
55+
}, [data]);
56+
57+
const MostActiveCourt = useMemo(() => {
58+
if (isUndefined(relData) || isUndefined(data)) {
59+
return null;
60+
}
61+
return getCourtWithMaxDifference(relData.courts, data.courts).name;
62+
}, [relData, data]);
63+
64+
return { MostActiveCourt, HighestDrawingChance, HighestRewardChance };
65+
};

web/src/hooks/queries/useHomePageQuery.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ const homePageQuery = graphql(`
1919
activeJurors
2020
cases
2121
}
22-
courts {
22+
courts(orderBy: id, orderDirection: asc) {
23+
id
2324
name
2425
numberDisputes
26+
feeForJuror
27+
stake
2528
}
2629
}
2730
`);

web/src/hooks/useBlockByTimestamp.tsx

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useState, useEffect } from "react";
2+
3+
import { ethers } from "ethers";
4+
import moment from "moment";
5+
6+
import { getChainRpcUrl } from "context/Web3Provider";
7+
8+
export const useBlockByTimestamp = (chainId, timestamp) => {
9+
const [blockNumber, setBlockNumber] = useState<number | null>(null);
10+
const [loading, setLoading] = useState(true);
11+
12+
useEffect(() => {
13+
const provider = new ethers.providers.JsonRpcProvider(getChainRpcUrl("https", chainId));
14+
15+
const getBlockByTimestamp = async () => {
16+
const getBlockWrapper = async (block) => {
17+
const blockData = await provider.getBlock(block);
18+
return {
19+
timestamp: blockData.timestamp,
20+
number: blockData.number,
21+
};
22+
};
23+
24+
const getBoundaries = async () => {
25+
const latestBlock = await getBlockWrapper("latest");
26+
const firstBlock = await getBlockWrapper(1);
27+
const blockTime = (latestBlock.timestamp - firstBlock.timestamp) / (latestBlock.number - 1);
28+
return { latestBlock, firstBlock, blockTime };
29+
};
30+
31+
const findBetter = async (date, predictedBlock, after, blockTime) => {
32+
if (await isBetterBlock(date, predictedBlock, after)) return predictedBlock.number;
33+
const difference = date.diff(moment.unix(predictedBlock.timestamp), "seconds");
34+
let skip = Math.ceil(difference / (blockTime === 0 ? 1 : blockTime));
35+
if (skip === 0) skip = difference < 0 ? -1 : 1;
36+
const nextPredictedBlock = await getBlockWrapper(predictedBlock.number + skip);
37+
blockTime = Math.abs(
38+
(predictedBlock.timestamp - nextPredictedBlock.timestamp) /
39+
(predictedBlock.number - nextPredictedBlock.number)
40+
);
41+
return findBetter(date, nextPredictedBlock, after, blockTime);
42+
};
43+
44+
const isBetterBlock = async (date, predictedBlock, after) => {
45+
const blockTime = moment.unix(predictedBlock.timestamp);
46+
if (after) {
47+
if (blockTime.isBefore(date)) return false;
48+
const previousBlock = await getBlockWrapper(predictedBlock.number - 1);
49+
if (blockTime.isSameOrAfter(date) && moment.unix(previousBlock.timestamp).isBefore(date)) return true;
50+
} else {
51+
if (blockTime.isSameOrAfter(date)) return false;
52+
const nextBlock = await getBlockWrapper(predictedBlock.number + 1);
53+
if (blockTime.isBefore(date) && moment.unix(nextBlock.timestamp).isSameOrAfter(date)) return true;
54+
}
55+
return false;
56+
};
57+
58+
try {
59+
const date = moment.utc(timestamp);
60+
const { latestBlock, firstBlock, blockTime } = await getBoundaries();
61+
62+
if (date.isBefore(moment.unix(firstBlock.timestamp))) {
63+
setBlockNumber(firstBlock.number);
64+
setLoading(false);
65+
return;
66+
}
67+
if (date.isSameOrAfter(moment.unix(latestBlock.timestamp))) {
68+
setBlockNumber(latestBlock.number);
69+
setLoading(false);
70+
return;
71+
}
72+
73+
const predictedBlockNumber = Math.ceil(date.diff(moment.unix(firstBlock.timestamp), "seconds") / blockTime);
74+
const predictedBlock = await getBlockWrapper(predictedBlockNumber);
75+
76+
const block = await findBetter(date, predictedBlock, true, blockTime);
77+
setBlockNumber(block);
78+
} catch (error) {
79+
console.error(error);
80+
} finally {
81+
setLoading(false);
82+
}
83+
};
84+
85+
getBlockByTimestamp();
86+
}, [chainId, timestamp]);
87+
88+
return { blockNumber, loading };
89+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React from "react";
2+
import styled, { css } from "styled-components";
3+
4+
import LawBalance from "svgs/icons/law-balance.svg";
5+
import LongArrowUp from "svgs/icons/long-arrow-up.svg";
6+
7+
import { useHomePageExtraStats } from "hooks/queries/useHomePageExtraStats";
8+
9+
import { landscapeStyle } from "styles/landscapeStyle";
10+
import { responsiveSize } from "styles/responsiveSize";
11+
12+
import ExtraStatsDisplay from "components/ExtraStatsDisplay";
13+
14+
const StyledCard = styled.div`
15+
padding-left: ${responsiveSize(16, 64)};
16+
padding-right: ${responsiveSize(16, 64)};
17+
display: flex;
18+
justify-content: space-between;
19+
flex-wrap: wrap;
20+
21+
${landscapeStyle(
22+
() => css`
23+
padding-bottom: 0px;
24+
gap: 8px;
25+
`
26+
)}
27+
`;
28+
29+
interface IStat {
30+
title: string;
31+
getText: (data) => string;
32+
icon: React.FC<React.SVGAttributes<SVGElement>>;
33+
}
34+
35+
const stats: IStat[] = [
36+
{
37+
title: "More Cases",
38+
getText: (data) => data.MostActiveCourt,
39+
icon: LongArrowUp,
40+
},
41+
{
42+
title: "Highest drawing chance",
43+
getText: (data) => data.HighestDrawingChance,
44+
icon: LongArrowUp,
45+
},
46+
{
47+
title: "Highest rewards chance",
48+
getText: (data) => data.HighestRewardChance,
49+
icon: LongArrowUp,
50+
},
51+
];
52+
53+
const ExtraStats = () => {
54+
const data = useHomePageExtraStats();
55+
return (
56+
<StyledCard>
57+
<ExtraStatsDisplay title="Activity (Last 7 days)" icon={LawBalance} />
58+
{stats.map(({ title, getText, icon }, i) => {
59+
return <ExtraStatsDisplay key={i} {...{ title, icon }} text={getText(data)} />;
60+
})}
61+
</StyledCard>
62+
);
63+
};
64+
65+
export default ExtraStats;

web/src/pages/Home/CourtOverview/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import styled from "styled-components";
33

44
import Chart from "./Chart";
5+
import ExtraStats from "./ExtraStats";
56
import Header from "./Header";
67
import Stats from "./Stats";
78

@@ -15,6 +16,7 @@ const CourtOverview: React.FC = () => (
1516
<Header />
1617
<Chart />
1718
<Stats />
19+
<ExtraStats />
1820
</Container>
1921
);
2022

web/src/utils/date.ts

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export function getOneYearAgoTimestamp(): number {
2020
return currentTime - 31536000; // One year in seconds
2121
}
2222

23+
export function getOneWeekAgoTimestamp(): number {
24+
const currentTime = new Date().getTime();
25+
return currentTime - 604800000; // One week in milliseconds
26+
}
27+
2328
export function formatDate(unixTimestamp: number, withTime = false): string {
2429
const date = new Date(unixTimestamp * 1000);
2530
const options: Intl.DateTimeFormatOptions = withTime

0 commit comments

Comments
 (0)