Skip to content

Commit 6dc0261

Browse files
committed
feat: top jurors staked in this court section
1 parent 2a92d72 commit 6dc0261

File tree

11 files changed

+394
-41
lines changed

11 files changed

+394
-41
lines changed

web/src/components/ConnectWallet/AccountDisplay.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const Container = styled.div`
1616
flex-direction: column;
1717
justify-content: space-between;
1818
height: auto;
19-
align-items: flex-start;
2019
gap: 8px;
2120
align-items: center;
2221
background-color: ${({ theme }) => theme.whiteBackground};
@@ -101,10 +100,8 @@ const ChainConnectionContainer = styled.div`
101100

102101
const StyledIdenticon = styled(Identicon)<{ size: `${number}` }>`
103102
align-items: center;
104-
svg {
105-
width: ${({ size }) => size + "px"};
106-
height: ${({ size }) => size + "px"};
107-
}
103+
width: ${({ size }) => size + "px"} !important;
104+
height: ${({ size }) => size + "px"} !important;
108105
`;
109106

110107
const StyledAvatar = styled.img<{ size: `${number}` }>`
@@ -115,12 +112,16 @@ const StyledAvatar = styled.img<{ size: `${number}` }>`
115112
height: ${({ size }) => size + "px"};
116113
`;
117114

115+
const StyledSmallLabel = styled.label`
116+
font-size: 14px !important;
117+
`;
118+
118119
interface IIdenticonOrAvatar {
119120
size?: `${number}`;
120121
address?: `0x${string}`;
121122
}
122123

123-
export const IdenticonOrAvatar: React.FC<IIdenticonOrAvatar> = ({ size = "16", address: propAddress }) => {
124+
export const IdenticonOrAvatar: React.FC<IIdenticonOrAvatar> = ({ size = "20", address: propAddress }) => {
124125
const { address: defaultAddress } = useAccount();
125126
const address = propAddress || defaultAddress;
126127

@@ -142,9 +143,10 @@ export const IdenticonOrAvatar: React.FC<IIdenticonOrAvatar> = ({ size = "16", a
142143

143144
interface IAddressOrName {
144145
address?: `0x${string}`;
146+
smallDisplay?: boolean;
145147
}
146148

147-
export const AddressOrName: React.FC<IAddressOrName> = ({ address: propAddress }) => {
149+
export const AddressOrName: React.FC<IAddressOrName> = ({ address: propAddress, smallDisplay }) => {
148150
const { address: defaultAddress } = useAccount();
149151
const address = propAddress || defaultAddress;
150152

@@ -153,7 +155,9 @@ export const AddressOrName: React.FC<IAddressOrName> = ({ address: propAddress }
153155
chainId: 1,
154156
});
155157

156-
return <label>{data ?? (isAddress(address!) ? shortenAddress(address) : address)}</label>;
158+
const content = data ?? (isAddress(address!) ? shortenAddress(address) : address);
159+
160+
return smallDisplay ? <StyledSmallLabel>{content}</StyledSmallLabel> : <label>{content}</label>;
157161
};
158162

159163
export const ChainDisplay: React.FC = () => {
@@ -166,7 +170,7 @@ const AccountDisplay: React.FC = () => {
166170
return (
167171
<Container>
168172
<AccountContainer>
169-
<IdenticonOrAvatar size="32" />
173+
<IdenticonOrAvatar size="20" />
170174
<AddressOrName />
171175
</AccountContainer>
172176
<ChainConnectionContainer>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { useGraphqlBatcher } from "context/GraphqlBatcher";
3+
import { graphql } from "src/graphql";
4+
import { TopStakedJurorsByCourtQuery, OrderDirection } from "src/graphql/graphql";
5+
6+
const topStakedJurorsByCourtQuery = graphql(`
7+
query TopStakedJurorsByCourt(
8+
$courtId: ID!
9+
$skip: Int
10+
$first: Int
11+
$orderBy: JurorTokensPerCourt_orderBy
12+
$orderDirection: OrderDirection
13+
$search: String
14+
) {
15+
jurorTokensPerCourts(
16+
where: { court_: { id: $courtId }, effectiveStake_gt: 0, juror_: { userAddress_contains_nocase: $search } }
17+
skip: $skip
18+
first: $first
19+
orderBy: $orderBy
20+
orderDirection: $orderDirection
21+
) {
22+
court {
23+
id
24+
}
25+
juror {
26+
id
27+
userAddress
28+
}
29+
effectiveStake
30+
}
31+
}
32+
`);
33+
34+
export const useTopStakedJurorsByCourt = (
35+
courtId: string,
36+
skip: number,
37+
first: number,
38+
orderBy: string,
39+
orderDirection: OrderDirection,
40+
search = ""
41+
) => {
42+
const { graphqlBatcher } = useGraphqlBatcher();
43+
return useQuery<TopStakedJurorsByCourtQuery>({
44+
queryKey: ["JurorsByCoherenceScore", courtId, skip, first, orderBy, orderDirection, search],
45+
staleTime: Infinity,
46+
queryFn: () =>
47+
graphqlBatcher.fetch({
48+
id: crypto.randomUUID(),
49+
document: topStakedJurorsByCourtQuery,
50+
variables: {
51+
courtId,
52+
skip,
53+
first,
54+
orderBy,
55+
orderDirection,
56+
search: search.toLowerCase(),
57+
},
58+
}),
59+
});
60+
};

web/src/pages/Courts/CourtDetails/Description.tsx

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import React, { useEffect } from "react";
22
import styled from "styled-components";
33

44
import ReactMarkdown from "react-markdown";
5-
import { Routes, Route, Navigate, useParams, useNavigate, useLocation } from "react-router-dom";
6-
5+
import { Routes, Route, Navigate, useParams, useNavigate, useLocation, useSearchParams } from "react-router-dom";
76
import { Tabs } from "@kleros/ui-components-library";
87

98
import { useCourtPolicy } from "queries/useCourtPolicy";
@@ -97,38 +96,34 @@ const Description: React.FC = () => {
9796
const { data: policy } = useCourtPolicy(id);
9897
const navigate = useNavigate();
9998
const location = useLocation();
99+
const [searchParams] = useSearchParams();
100+
const suffix = searchParams.toString() ? `?${searchParams.toString()}` : "";
100101
const currentPathName = location.pathname.split("/").at(-1);
101102

102103
const filteredTabs = TABS.filter(({ isVisible }) => isVisible(policy));
103104
const currentTab = TABS.findIndex(({ path }) => path === currentPathName);
104105

105-
const handleTabChange = (index: number) => {
106-
navigate(TABS[index].path);
106+
const handleTabChange = (i: number) => {
107+
navigate(`${TABS[i].path}${suffix}`);
107108
};
108-
109109
useEffect(() => {
110110
if (currentPathName && !filteredTabs.map((t) => t.path).includes(currentPathName) && filteredTabs.length > 0) {
111-
navigate(filteredTabs[0].path, { replace: true });
111+
navigate(`${filteredTabs[0].path}${suffix}`, { replace: true });
112112
}
113-
}, [policy, currentPathName, filteredTabs, navigate]);
114-
115-
return (
116-
<>
117-
{policy ? (
118-
<Container id="description">
119-
<StyledTabs currentValue={currentTab} items={filteredTabs} callback={handleTabChange} />
120-
<TextContainer>
121-
<Routes>
122-
<Route path="purpose" element={formatMarkdown(policy?.purpose)} />
123-
<Route path="skills" element={formatMarkdown(policy?.requiredSkills)} />
124-
<Route path="policy" element={formatMarkdown(policy?.rules)} />
125-
<Route path="*" element={<Navigate to={filteredTabs.length > 0 ? filteredTabs[0].path : ""} replace />} />
126-
</Routes>
127-
</TextContainer>
128-
</Container>
129-
) : null}
130-
</>
131-
);
113+
}, [policy, currentPathName, filteredTabs, navigate, suffix]);
114+
return policy ? (
115+
<Container id="description">
116+
<StyledTabs currentValue={currentTab} items={filteredTabs} callback={handleTabChange} />
117+
<TextContainer>
118+
<Routes>
119+
<Route path="purpose" element={formatMarkdown(policy?.purpose)} />
120+
<Route path="skills" element={formatMarkdown(policy?.requiredSkills)} />
121+
<Route path="policy" element={formatMarkdown(policy?.rules)} />
122+
<Route path="*" element={<Navigate to={filteredTabs.length > 0 ? filteredTabs[0].path : ""} replace />} />
123+
</Routes>
124+
</TextContainer>
125+
</Container>
126+
) : null;
132127
};
133128

134129
const formatMarkdown = (markdown?: string) =>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { responsiveSize } from "styles/responsiveSize";
5+
6+
const Container = styled.div`
7+
display: flex;
8+
width: 100%;
9+
background-color: ${({ theme }) => theme.lightBlue};
10+
border: 1px solid ${({ theme }) => theme.stroke};
11+
border-top-left-radius: 3px;
12+
border-top-right-radius: 3px;
13+
padding: 18px 24px;
14+
justify-content: space-between;
15+
margin-top: ${responsiveSize(12, 16)};
16+
`;
17+
18+
const StyledLabel = styled.label`
19+
font-size: 14px;
20+
color: ${({ theme }) => theme.secondaryText};
21+
`;
22+
23+
const Header: React.FC = () => {
24+
return (
25+
<Container>
26+
<StyledLabel>Juror</StyledLabel>
27+
<StyledLabel>Stake</StyledLabel>
28+
</Container>
29+
);
30+
};
31+
32+
export default Header;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { hoverShortTransitionTiming } from "styles/commonStyles";
5+
6+
import JurorTitle from "pages/Home/TopJurors/JurorCard/JurorTitle";
7+
import Stake from "./Stake";
8+
9+
const Container = styled.div`
10+
${hoverShortTransitionTiming}
11+
display: flex;
12+
justify-content: space-between;
13+
width: 100%;
14+
background-color: ${({ theme }) => theme.whiteBackground};
15+
border: 1px solid ${({ theme }) => theme.stroke};
16+
border-top: none;
17+
align-items: center;
18+
padding: 18px 24px;
19+
20+
:hover {
21+
background-color: ${({ theme }) => theme.lightGrey}BB;
22+
}
23+
`;
24+
25+
interface IJurorCard {
26+
address: string;
27+
effectiveStake: string;
28+
}
29+
30+
const JurorCard: React.FC<IJurorCard> = ({ address, effectiveStake }) => {
31+
return (
32+
<Container>
33+
<JurorTitle {...{ address }} smallDisplay />
34+
<Stake {...{ effectiveStake }} />
35+
</Container>
36+
);
37+
};
38+
39+
export default JurorCard;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { formatPNK } from "utils/format";
5+
6+
interface IStake {
7+
effectiveStake: string;
8+
}
9+
10+
const StyledLabel = styled.label`
11+
font-size: 14px;
12+
color: ${({ theme }) => theme.primaryText};
13+
`;
14+
15+
const Stake: React.FC<IStake> = ({ effectiveStake }) => {
16+
return <StyledLabel> {formatPNK(BigInt(effectiveStake))} </StyledLabel>;
17+
};
18+
export default Stake;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React, { useEffect, useMemo, useRef, useState } from "react";
2+
import { useParams, useSearchParams } from "react-router-dom";
3+
import styled from "styled-components";
4+
import { responsiveSize } from "styles/responsiveSize";
5+
import { isUndefined } from "utils/index";
6+
import { useTopStakedJurorsByCourt } from "queries/useTopStakedJurorsByCourt";
7+
import { OrderDirection } from "src/graphql/graphql";
8+
import { SkeletonDisputeListItem } from "components/StyledSkeleton";
9+
import JurorCard from "./JurorCard";
10+
import Header from "./Header";
11+
import { ListContainer as BaseListContainer } from "pages/Home/TopJurors";
12+
13+
const ListContainer = styled(BaseListContainer)`
14+
overflow: visible;
15+
`;
16+
17+
const CardsWrapper = styled.div`
18+
max-height: 520px;
19+
overflow-y: hidden;
20+
21+
&:hover {
22+
overflow-y: auto;
23+
}
24+
`;
25+
26+
const StyledLabel = styled.label`
27+
display: flex;
28+
font-size: 16px;
29+
margin-top: ${responsiveSize(12, 20)};
30+
`;
31+
32+
const PER_PAGE = 30;
33+
34+
const DisplayJurors: React.FC = () => {
35+
const { id: courtId, order } = useParams();
36+
const [searchParams] = useSearchParams();
37+
const searchValue = searchParams.get("topSearch") ?? "";
38+
const [page, setPage] = useState(0);
39+
const skip = page * PER_PAGE;
40+
const { data, isFetching } = useTopStakedJurorsByCourt(
41+
courtId,
42+
skip,
43+
PER_PAGE,
44+
"effectiveStake",
45+
order === "asc" ? OrderDirection.Asc : OrderDirection.Desc,
46+
searchValue
47+
);
48+
const [acc, setAcc] = useState<{ id: string; userAddress: string; effectiveStake: string }[]>([]);
49+
50+
useEffect(() => {
51+
setPage(0);
52+
setAcc([]);
53+
}, [searchValue, courtId, order]);
54+
55+
useEffect(() => {
56+
const chunk =
57+
data?.jurorTokensPerCourts?.map((j) => ({
58+
id: j.juror.id,
59+
userAddress: j.juror.userAddress,
60+
effectiveStake: j.effectiveStake,
61+
})) ?? [];
62+
if (chunk.length) setAcc((prev) => [...prev, ...chunk]);
63+
}, [data]);
64+
65+
const sentinelRef = useRef<HTMLDivElement | null>(null);
66+
67+
useEffect(() => {
68+
const sentinel = sentinelRef.current;
69+
if (!sentinel) return;
70+
const obs = new IntersectionObserver(
71+
([e]) => {
72+
if (e.isIntersecting && !isFetching && acc.length % PER_PAGE === 0) setPage((p) => p + 1);
73+
},
74+
{ threshold: 0.1 }
75+
);
76+
obs.observe(sentinel);
77+
return () => obs.disconnect();
78+
}, [isFetching, acc.length]);
79+
80+
const jurors = useMemo(() => acc, [acc]);
81+
82+
return (
83+
<>
84+
{!isUndefined(jurors) && jurors.length === 0 && !isFetching ? (
85+
<StyledLabel>No jurors found</StyledLabel>
86+
) : (
87+
<ListContainer>
88+
<Header />
89+
<CardsWrapper>
90+
{jurors.map((j) => (
91+
<JurorCard key={j.id} address={j.id} {...j} />
92+
))}
93+
{isFetching && [...Array(9)].map((_, i) => <SkeletonDisputeListItem key={`s-${i}`} />)}
94+
<div ref={sentinelRef} />
95+
</CardsWrapper>
96+
</ListContainer>
97+
)}
98+
</>
99+
);
100+
};
101+
102+
export default DisplayJurors;

0 commit comments

Comments
 (0)