Skip to content

Commit ca9a15c

Browse files
authored
Merge pull request #1969 from kleros/feat(web)/top-jurors-staked-in-this-court
feat(web): jurors staked in this court section
2 parents 886c27e + 9e0a7bd commit ca9a15c

File tree

14 files changed

+443
-66
lines changed

14 files changed

+443
-66
lines changed

web/src/components/CasesDisplay/index.tsx

+22-5
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 { useLocation } from "react-router-dom";
5+
import { useAccount } from "wagmi";
56

67
import ArrowIcon from "svgs/icons/arrow.svg";
78

@@ -29,6 +30,12 @@ const StyledLabel = styled.label`
2930
font-size: ${responsiveSize(14, 16)};
3031
`;
3132

33+
const LinksContainer = styled.div`
34+
display: flex;
35+
flex-direction: row;
36+
gap: 16px;
37+
`;
38+
3239
interface ICasesDisplay extends ICasesGrid {
3340
numberDisputes?: number;
3441
numberClosedDisputes?: number;
@@ -48,15 +55,25 @@ const CasesDisplay: React.FC<ICasesDisplay> = ({
4855
totalPages,
4956
}) => {
5057
const location = useLocation();
58+
const { isConnected, address } = useAccount();
59+
const profileLink = isConnected && address ? `/profile/1/desc/all?address=${address}` : null;
60+
5161
return (
5262
<div {...{ className }}>
5363
<TitleContainer className="title">
5464
<StyledTitle>{title}</StyledTitle>
55-
{location.pathname.startsWith("/cases/display/1/desc/all") ? (
56-
<StyledArrowLink to={"/resolver"}>
57-
Create a case <ArrowIcon />
58-
</StyledArrowLink>
59-
) : null}
65+
<LinksContainer>
66+
{location.pathname.startsWith("/cases/display") && profileLink ? (
67+
<StyledArrowLink to={profileLink}>
68+
My Cases <ArrowIcon />
69+
</StyledArrowLink>
70+
) : null}
71+
{location.pathname.startsWith("/cases/display") ? (
72+
<StyledArrowLink to={"/resolver"}>
73+
Create a case <ArrowIcon />
74+
</StyledArrowLink>
75+
) : null}
76+
</LinksContainer>
6077
</TitleContainer>
6178
<Search />
6279
<StatsAndFilters totalDisputes={numberDisputes || 0} closedDisputes={numberClosedDisputes || 0} />

web/src/components/ConnectWallet/AccountDisplay.tsx

+13-9
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>
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: ["TopStakedJurorsByCourt", courtId, skip, first, orderBy, orderDirection, search],
45+
staleTime: 10 * 60 * 1000,
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/layout/Header/navbar/Explore.tsx

+26-19
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React from "react";
1+
import React, { useMemo } from "react";
22
import styled, { css } from "styled-components";
33
import { landscapeStyle } from "styles/landscapeStyle";
44

55
import { Link, useLocation } from "react-router-dom";
6+
import { useAccount } from "wagmi";
67

78
import { useOpenContext } from "../MobileHeader";
89

@@ -50,35 +51,41 @@ const StyledLink = styled(Link)<{ isActive: boolean; isMobileNavbar?: boolean }>
5051
)};
5152
`;
5253

53-
const links = [
54-
{ to: "/", text: "Home" },
55-
{ to: "/cases/display/1/desc/all", text: "Cases" },
56-
{ to: "/courts", text: "Courts" },
57-
{ to: "/jurors/1/desc/all", text: "Jurors" },
58-
{ to: "/get-pnk", text: "Get PNK" },
59-
];
60-
6154
interface IExplore {
6255
isMobileNavbar?: boolean;
6356
}
6457

6558
const Explore: React.FC<IExplore> = ({ isMobileNavbar }) => {
6659
const location = useLocation();
6760
const { toggleIsOpen } = useOpenContext();
61+
const { isConnected, address } = useAccount();
62+
63+
const navLinks = useMemo(() => {
64+
const base = [
65+
{ to: "/", text: "Home" },
66+
{ to: "/cases/display/1/desc/all", text: "Cases" },
67+
{ to: "/courts", text: "Courts" },
68+
{ to: "/jurors/1/desc/all", text: "Jurors" },
69+
{ to: "/get-pnk", text: "Get PNK" },
70+
];
71+
if (isConnected && address) {
72+
base.push({ to: `/profile/1/desc/all?address=${address}`, text: "My Profile" });
73+
}
74+
return base;
75+
}, [isConnected, address]);
6876

6977
return (
7078
<Container>
7179
<Title>Explore</Title>
72-
{links.map(({ to, text }) => (
73-
<StyledLink
74-
key={text}
75-
onClick={toggleIsOpen}
76-
isActive={to === "/" ? location.pathname === "/" : location.pathname.split("/")[1] === to.split("/")[1]}
77-
{...{ to, isMobileNavbar }}
78-
>
79-
{text}
80-
</StyledLink>
81-
))}
80+
{navLinks.map(({ to, text }) => {
81+
const pathBase = to.split("?")[0];
82+
const isActive = pathBase === "/" ? location.pathname === "/" : location.pathname.startsWith(pathBase);
83+
return (
84+
<StyledLink key={text} onClick={toggleIsOpen} isActive={isActive} {...{ to, isMobileNavbar }}>
85+
{text}
86+
</StyledLink>
87+
);
88+
})}
8289
</Container>
8390
);
8491
};

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

+20-25
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) =>
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>PNK Staked</StyledLabel>
28+
</Container>
29+
);
30+
};
31+
32+
export default Header;
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;

0 commit comments

Comments
 (0)