Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: jurors page #1840

Merged
merged 21 commits into from
Jan 30, 2025
Merged
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ed63810
feat: initial jurors page setup, jurors explore navlink, small stylin…
kemuru Jan 20, 2025
1b22294
feat: filter by totalresolvedvotes bigger than 0, types fix
kemuru Jan 21, 2025
63c308b
fix: subgraph bug which has not counting missed votes
kemuru Jan 22, 2025
ac069a3
Merge branch 'dev' into feat(web)/jurors-page
kemuru Jan 22, 2025
118feb6
Merge branch 'dev' into feat(web)/jurors-page
kemuru Jan 22, 2025
af51757
Merge branch 'dev' into feat(web)/jurors-page
kemuru Jan 22, 2025
db29a3d
feat: myprofile links, subgraph changes, pagination, query optimizati…
kemuru Jan 25, 2025
c95ee8a
Merge branch 'dev' into feat(web)/jurors-page
kemuru Jan 25, 2025
f2867a2
feat: add subgraph field, ranking icon, fix extra stats bug, filters,…
kemuru Jan 27, 2025
c749b2d
fix: nitpick text change
kemuru Jan 27, 2025
132743a
chore: add arrow back into the accordiontitle
kemuru Jan 27, 2025
07f83fa
chore: layout improvement
kemuru Jan 27, 2025
94c7637
fix: search to lower case in query
kemuru Jan 28, 2025
22e54d7
chore: move coherence percent function to utils, change name for clarity
kemuru Jan 28, 2025
81a3412
fix: layout shift if there is no rank, hide pagination if searching
kemuru Jan 28, 2025
b5d969b
Merge branch 'dev' into feat(web)/jurors-page
kemuru Jan 28, 2025
535508a
fix: few code smells
kemuru Jan 28, 2025
d04f0e1
chore: change label in mobile if search is active
kemuru Jan 28, 2025
876e208
Merge branch 'dev' into feat(web)/jurors-page
alcercu Jan 29, 2025
e6875eb
chore: subgraphs redeploy, scripts tweaks
jaybuidl Jan 30, 2025
2bb0a19
chore: txt
kemuru Jan 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: myprofile links, subgraph changes, pagination, query optimizati…
…on, levels miniguides tweak etc
kemuru committed Jan 25, 2025

Verified

This commit was signed with the committer’s verified signature.
kemuru Marino
commit db29a3dc41197779310e60d28e481276166dfb50
1 change: 1 addition & 0 deletions subgraph/core/schema.graphql
Original file line number Diff line number Diff line change
@@ -237,6 +237,7 @@ type Counter @entity {
casesVoting: BigInt!
casesRuled: BigInt!
casesAppealing: BigInt!
totalLeaderboardJurors: BigInt!
}

type FeeToken @entity {
15 changes: 13 additions & 2 deletions subgraph/core/src/KlerosCore.ts
Original file line number Diff line number Diff line change
@@ -18,13 +18,19 @@ import { createCourtFromEvent } from "./entities/Court";
import { createDisputeKitFromEvent, filterSupportedDisputeKits } from "./entities/DisputeKit";
import { createDisputeFromEvent } from "./entities/Dispute";
import { createRoundFromRoundInfo, updateRoundTimeline } from "./entities/Round";
import { updateCases, updateCasesAppealing, updateCasesRuled, updateCasesVoting } from "./datapoint";
import {
updateCases,
updateCasesAppealing,
updateCasesRuled,
updateCasesVoting,
updateTotalLeaderboardJurors,
} from "./datapoint";
import { addUserActiveDispute, computeCoherenceScore, ensureUser } from "./entities/User";
import { updateJurorStake } from "./entities/JurorTokensPerCourt";
import { createDrawFromEvent } from "./entities/Draw";
import { updateTokenAndEthShiftFromEvent } from "./entities/TokenAndEthShift";
import { updateArbitrableCases } from "./entities/Arbitrable";
import { ClassicVote, Court, Dispute, Draw, Round, User } from "../generated/schema";
import { ClassicVote, Counter, Court, Dispute, Draw, Round, User } from "../generated/schema";
import { BigInt } from "@graphprotocol/graph-ts";
import { updatePenalty } from "./entities/Penalty";
import { ensureFeeToken } from "./entities/FeeToken";
@@ -142,6 +148,11 @@ export function handleNewPeriod(event: NewPeriod): void {
const juror = ensureUser(draw.juror);
juror.totalResolvedVotes = juror.totalResolvedVotes.plus(ONE);

// Increment totalLeaderboardJurors in the Counter entity if this is the first resolved vote for the juror
if (juror.totalResolvedVotes.equals(ONE)) {
updateTotalLeaderboardJurors(ONE, event.block.timestamp);
}

// Since this is a ClassicVote entity, this will only work for the Classic DisputeKit (which has ID "1").
const vote = ClassicVote.load(`${round.disputeKit}-${draw.id}`);

6 changes: 6 additions & 0 deletions subgraph/core/src/datapoint.ts
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ const VARIABLES = [
"casesVoting",
"casesRuled",
"casesAppealing",
"totalLeaderboardJurors",
];

function updateDataPoint(delta: BigInt, timestamp: BigInt, variable: string): void {
@@ -43,6 +44,7 @@ function checkFirstDayActivity(): void {
counter.casesVoting = ZERO;
counter.casesRuled = ZERO;
counter.casesAppealing = ZERO;
counter.totalLeaderboardJurors = ZERO;
counter.save();
}
}
@@ -86,3 +88,7 @@ export function updateCasesRuled(delta: BigInt, timestamp: BigInt): void {
export function updateCasesAppealing(delta: BigInt, timestamp: BigInt): void {
updateDataPoint(delta, timestamp, "casesAppealing");
}

export function updateTotalLeaderboardJurors(delta: BigInt, timestamp: BigInt): void {
updateDataPoint(delta, timestamp, "totalLeaderboardJurors");
}
2 changes: 1 addition & 1 deletion web/src/app.tsx
Original file line number Diff line number Diff line change
@@ -65,7 +65,7 @@ const App: React.FC = () => {
}
/>
<Route
path="jurors/*"
path="jurors/:page/:order/:filter"
element={
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
<Jurors />
6 changes: 3 additions & 3 deletions web/src/components/Popup/MiniGuides/JurorLevels.tsx
Original file line number Diff line number Diff line change
@@ -35,16 +35,16 @@ const leftPageContents = [
title: "Juror Level 1: Pythagoras",
paragraphs: [
"Jurors are classified into distinct levels according to their performance starting from Level 1.",
"Level 1: Jurors with 0 cases arbitrated, OR Jurors with ≥ 1 case arbitrated with 0-70% of coherent votes.",
"Level 1: Jurors with ≥ 1 case arbitrated with 0-70% of coherent votes.",
],
},
{
title: "Juror Level 2: Socrates",
paragraphs: ["Level 2: Jurors with ≥ 3 cases arbitrated with 70%-80% of coherent votes."],
paragraphs: ["Level 2: Jurors with ≥ 3 cases arbitrated with more than 70% coherent votes."],
},
{
title: "Juror Level 3: Plato",
paragraphs: ["Level 3: Jurors with ≥ 7 cases arbitrated with 80%-90% of coherent votes."],
paragraphs: ["Level 3: Jurors with ≥ 7 cases arbitrated with more than 80% of coherent votes."],
},
{
title: "Juror Level 4: Aristotle",
50 changes: 50 additions & 0 deletions web/src/hooks/queries/useJurorsByCoherenceScore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useQuery } from "@tanstack/react-query";

import { useGraphqlBatcher } from "context/GraphqlBatcher";
import { isUndefined } from "utils/index";

import { graphql } from "src/graphql";
import { JurorsByCoherenceScoreQuery } from "src/graphql/graphql";
export type { JurorsByCoherenceScoreQuery };

const jurorsByCoherenceScoreQuery = graphql(`
query JurorsByCoherenceScore($skip: Int, $first: Int, $orderBy: User_orderBy, $orderDirection: OrderDirection) {
users(
first: $first
skip: $skip
orderBy: $orderBy
orderDirection: $orderDirection
where: { totalResolvedVotes_gt: 0 }
) {
id
coherenceScore
totalCoherentVotes
totalResolvedVotes
totalResolvedDisputes
}
}
`);

export const useJurorsByCoherenceScore = (skip = 0, first = 20, orderBy: string, orderDirection: string) => {
const isEnabled = !isUndefined(first);
const { graphqlBatcher } = useGraphqlBatcher();

return useQuery<JurorsByCoherenceScoreQuery>({
queryKey: [`JurorsByCoherenceScore`, skip, first, orderBy, orderDirection],
enabled: isEnabled,
staleTime: Infinity,
queryFn: async () =>
isEnabled
? await graphqlBatcher.fetch({
id: crypto.randomUUID(),
document: jurorsByCoherenceScoreQuery,
variables: {
skip,
first,
orderBy,
orderDirection,
},
})
: undefined,
});
};
43 changes: 0 additions & 43 deletions web/src/hooks/queries/useTopUsersByCoherenceScore.ts

This file was deleted.

32 changes: 32 additions & 0 deletions web/src/hooks/queries/useTotalLeaderboardJurors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useQuery } from "@tanstack/react-query";

import { useGraphqlBatcher } from "context/GraphqlBatcher";

import { graphql } from "src/graphql";
import { TotalLeaderboardJurorsQuery } from "src/graphql/graphql";
export type { TotalLeaderboardJurorsQuery };

const totalLeaderboardJurorsQuery = graphql(`
query TotalLeaderboardJurors($id: ID!) {
counter(id: $id) {
totalLeaderboardJurors
}
}
`);

export const useTotalLeaderboardJurors = () => {
const { graphqlBatcher } = useGraphqlBatcher();

return useQuery<TotalLeaderboardJurorsQuery>({
queryKey: [`TotalLeaderboardJurors`],
staleTime: Infinity,
queryFn: async () =>
await graphqlBatcher.fetch({
id: crypto.randomUUID(),
document: totalLeaderboardJurorsQuery,
variables: {
id: 0,
},
}),
});
};
2 changes: 1 addition & 1 deletion web/src/layout/Header/navbar/Explore.tsx
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ const links = [
{ to: "/", text: "Home" },
{ to: "/cases/display/1/desc/all", text: "Cases" },
{ to: "/courts", text: "Courts" },
{ to: "/jurors", text: "Jurors" },
{ to: "/jurors/1/desc/all", text: "Jurors" },
{ to: "/get-pnk", text: "Get PNK" },
];

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";
import styled from "styled-components";

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

import { AddressOrName, IdenticonOrAvatar } from "components/ConnectWallet/AccountDisplay";
import { StyledArrowLink } from "components/StyledArrowLink";
import { ISettings } from "../../../index";

const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding: 16px 32px;
gap: 24px;
border: 1px solid ${({ theme }) => theme.stroke};
border-radius: 30px;
> label {
color: ${({ theme }) => theme.primaryText};
font-size: 16px;
font-weight: 600;
}
`;

const AvatarAndAddressContainer = styled.div`
display: flex;
flex-direction: row;
gap: 8px;
`;

const ReStyledArrowLink = styled(StyledArrowLink)`
font-size: 14px;
> svg {
height: 14px;
width: 14px;
}
`;

const WalletAndProfile: React.FC<ISettings> = ({ toggleIsSettingsOpen }) => {
return (
<Container>
<AvatarAndAddressContainer>
<IdenticonOrAvatar />
<AddressOrName />
</AvatarAndAddressContainer>
<ReStyledArrowLink to={"/profile/1/desc/all"} onClick={toggleIsSettingsOpen}>
My Profile <ArrowIcon />
</ReStyledArrowLink>
</Container>
);
};
export default WalletAndProfile;
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { useMemo } from "react";
import React from "react";
import styled from "styled-components";

import { useAccount, useDisconnect } from "wagmi";

import { Button } from "@kleros/ui-components-library";

import { AddressOrName, ChainDisplay, IdenticonOrAvatar } from "components/ConnectWallet/AccountDisplay";
import { ChainDisplay } from "components/ConnectWallet/AccountDisplay";
import { EnsureChain } from "components/EnsureChain";
import WalletAndProfile from "./WalletAndProfile";
import { ISettings } from "../../../index";

const Container = styled.div`
display: flex;
@@ -33,22 +35,6 @@ const StyledChainContainer = styled.div`
}
`;

const StyledAddressContainer = styled.div`
display: flex;
justify-content: center;
> label {
color: ${({ theme }) => theme.primaryText};
font-size: 16px;
font-weight: 600;
}
`;

const StyledAvatarContainer = styled.div`
display: flex;
justify-content: center;
margin-top: 12px;
`;

const StyledButton = styled.div`
display: flex;
justify-content: center;
@@ -67,47 +53,24 @@ const UserContainer = styled.div`
gap: 12px;
`;

const StyledA = styled.a`
text-decoration: none;
label {
cursor: pointer;
color: ${({ theme }) => theme.primaryBlue};
}
:hover {
text-decoration: underline;
}
`;

export const DisconnectWalletButton: React.FC = () => {
const { disconnect } = useDisconnect();
return <Button text={`Disconnect`} onClick={() => disconnect()} />;
};

const General: React.FC = () => {
const { address, chain } = useAccount();

const addressExplorerLink = useMemo(() => {
return `${chain?.blockExplorers?.default.url}/address/${address}`;
}, [address, chain]);
const General: React.FC<ISettings> = ({ toggleIsSettingsOpen }) => {
const { address } = useAccount();

return (
<EnsureChainContainer>
<EnsureChain>
<Container>
{address && (
<UserContainer>
<StyledAvatarContainer>
<IdenticonOrAvatar size="48" />
</StyledAvatarContainer>
<StyledAddressContainer>
<StyledA href={addressExplorerLink} rel="noreferrer" target="_blank">
<AddressOrName />
</StyledA>
</StyledAddressContainer>
<StyledChainContainer>
<ChainDisplay />
</StyledChainContainer>
<WalletAndProfile {...{ toggleIsSettingsOpen }} />
<StyledButton>
<DisconnectWalletButton />
</StyledButton>
6 changes: 5 additions & 1 deletion web/src/layout/Header/navbar/Menu/Settings/index.tsx
Original file line number Diff line number Diff line change
@@ -91,7 +91,11 @@ const Settings: React.FC<ISettings> = ({ toggleIsSettingsOpen, initialTab }) =>
setCurrentTab(n);
}}
/>
{currentTab === 0 ? <General /> : <NotificationSettings {...{ toggleIsSettingsOpen }} />}
{currentTab === 0 ? (
<General {...{ toggleIsSettingsOpen }} />
) : (
<NotificationSettings {...{ toggleIsSettingsOpen }} />
)}
</Container>
);
};
2 changes: 1 addition & 1 deletion web/src/pages/Home/TopJurors/JurorCard/Coherence.tsx
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ const Container = styled.div`
justify-content: center;
`;

const getPercent = (num: number, den: number): string => {
export const getPercent = (num: number, den: number): string => {
if (den === 0) return "0%";
return `${Math.floor((num * 100) / den)}%`;
};
2 changes: 1 addition & 1 deletion web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ const DesktopCard: React.FC<IDesktopCard> = ({
<JurorTitle address={address} />
<Rewards address={address} />
<Coherence {...{ totalCoherentVotes, totalResolvedVotes }} />
<JurorLevel {...{ coherenceScore, totalResolvedDisputes }} />
<JurorLevel {...{ totalCoherentVotes, totalResolvedVotes, totalResolvedDisputes }} />
</Container>
);
};
9 changes: 6 additions & 3 deletions web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import { getUserLevelData } from "utils/userLevelCalculation";
import { landscapeStyle } from "styles/landscapeStyle";

import PixelArt from "pages/Profile/JurorInfo/PixelArt";
import { getPercent } from "./Coherence";

const Container = styled.div`
display: flex;
@@ -39,12 +40,14 @@ const StyledLabel = styled.label`
`;

interface IJurorLevel {
coherenceScore: string;
totalCoherentVotes: string;
totalResolvedVotes: string;
totalResolvedDisputes: string;
}

const JurorLevel: React.FC<IJurorLevel> = ({ coherenceScore, totalResolvedDisputes }) => {
const userLevelData = getUserLevelData(Number(coherenceScore), Number(totalResolvedDisputes));
const JurorLevel: React.FC<IJurorLevel> = ({ totalCoherentVotes, totalResolvedVotes, totalResolvedDisputes }) => {
const coherencePercentage = getPercent(Number(totalCoherentVotes), Number(totalResolvedVotes));
const userLevelData = getUserLevelData(coherencePercentage, Number(totalResolvedDisputes));
const level = userLevelData.level;

return (
25 changes: 19 additions & 6 deletions web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from "react";
import styled from "styled-components";

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

import { IdenticonOrAvatar, AddressOrName } from "components/ConnectWallet/AccountDisplay";
import { InternalLink } from "components/InternalLink";
import { StyledArrowLink } from "components/StyledArrowLink";
import { useAccount } from "wagmi";

const Container = styled.div`
display: flex;
@@ -20,10 +23,14 @@ const Container = styled.div`
}
`;

const StyledInternalLink = styled(InternalLink)`
export const ReStyledArrowLink = styled(StyledArrowLink)`
label {
cursor: pointer;
color: ${({ theme }) => theme.primaryBlue};
}
:hover {
label {
cursor: pointer;
color: ${({ theme }) => theme.secondaryBlue};
}
}
@@ -34,15 +41,21 @@ interface IJurorTitle {
}

const JurorTitle: React.FC<IJurorTitle> = ({ address }) => {
const profileLink = `/profile/1/desc/all?address=${address}`;
const { isConnected, address: connectedAddress } = useAccount();
const profileLink =
isConnected && connectedAddress?.toLowerCase() === address.toLowerCase()
? "/profile/1/desc/all"
: `/profile/1/desc/all?address=${address}`;

return (
<Container>
<IdenticonOrAvatar address={address} />
<StyledInternalLink to={profileLink}>
<ReStyledArrowLink to={profileLink}>
<AddressOrName address={address} />
</StyledInternalLink>
<ArrowIcon />
</ReStyledArrowLink>
</Container>
);
};

export default JurorTitle;
2 changes: 1 addition & 1 deletion web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx
Original file line number Diff line number Diff line change
@@ -101,7 +101,7 @@ const MobileCard: React.FC<IMobileCard> = ({
<Rank rank={rank} />
<JurorTitle address={address} />
</RankAndTitle>
<JurorLevel {...{ coherenceScore, totalResolvedDisputes }} />
<JurorLevel {...{ totalCoherentVotes, totalResolvedVotes, totalResolvedDisputes }} />
</TopSide>
<BottomSide>
<HeaderRewardsAndRewards>
4 changes: 2 additions & 2 deletions web/src/pages/Home/TopJurors/index.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import styled, { css } from "styled-components";

import { isUndefined } from "utils/index";

import { useTopUsersByCoherenceScore } from "queries/useTopUsersByCoherenceScore";
import { useJurorsByCoherenceScore } from "queries/useJurorsByCoherenceScore";

import { landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";
@@ -40,7 +40,7 @@ export const StyledLabel = styled.label`
`;

const TopJurors: React.FC = () => {
const { data: queryJurors } = useTopUsersByCoherenceScore();
const { data: queryJurors } = useJurorsByCoherenceScore(0, 5, "coherenceScore", "desc");

const topJurors = queryJurors?.users?.map((juror, index) => ({
...juror,
81 changes: 81 additions & 0 deletions web/src/pages/Jurors/DisplayJurors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useMemo } from "react";
import { useParams, useNavigate } from "react-router-dom";
import styled from "styled-components";

import { isUndefined } from "utils/index";
import { StandardPagination } from "@kleros/ui-components-library";

import { useJurorsByCoherenceScore } from "queries/useJurorsByCoherenceScore";
import useIsDesktop from "hooks/useIsDesktop";

import { OrderDirection } from "src/graphql/graphql";

import { SkeletonDisputeListItem } from "components/StyledSkeleton";
import JurorCard from "../Home/TopJurors/JurorCard";
import { ListContainer, StyledLabel } from "../Home/TopJurors";
import Header from "../Home/TopJurors/Header";

interface IDisplayJurors {
totalLeaderboardJurors: number;
}

const StyledPagination = styled(StandardPagination)`
margin-top: 24px;
margin-left: auto;
margin-right: auto;
`;

const DisplayJurors: React.FC<IDisplayJurors> = ({ totalLeaderboardJurors }) => {
const { page, order, filter } = useParams();
const navigate = useNavigate();
const isDesktop = useIsDesktop();
const jurorsPerPage = isDesktop ? 20 : 10;
const currentPage = parseInt(page ?? "1");

const jurorSkip = jurorsPerPage * (currentPage - 1);

const { data: queryJurors } = useJurorsByCoherenceScore(
jurorSkip,
jurorsPerPage,
"coherenceScore",
order === "asc" ? OrderDirection.Asc : OrderDirection.Desc
);

const jurors = useMemo(
() =>
queryJurors?.users?.map((juror, index) => ({
...juror,
rank: jurorSkip + index + 1,
})),
[queryJurors, jurorSkip]
);

const totalPages = useMemo(
() => Math.ceil(totalLeaderboardJurors / jurorsPerPage),
[totalLeaderboardJurors, jurorsPerPage]
);

const handlePageChange = (newPage: number) => {
navigate(`/jurors/${newPage}/${order}/${filter}`);
};

return (
<>
{!isUndefined(jurors) && jurors.length === 0 ? (
<StyledLabel>There are no jurors staked yet.</StyledLabel>
) : (
<>
<ListContainer>
<Header />
{!isUndefined(jurors)
? jurors.map((juror) => <JurorCard key={juror.rank} address={juror.id} {...juror} />)
: [...Array(jurorsPerPage)].map((_, i) => <SkeletonDisputeListItem key={i} />)}
</ListContainer>
<StyledPagination currentPage={currentPage} numPages={totalPages} callback={handlePageChange} />
</>
)}
</>
);
};

export default DisplayJurors;
36 changes: 9 additions & 27 deletions web/src/pages/Jurors/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useRef, useState } from "react";
import styled, { css } from "styled-components";
import styled from "styled-components";

import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useDebounce } from "react-use";
@@ -8,24 +8,8 @@ import { Searchbar } from "@kleros/ui-components-library";
import { isEmpty } from "utils/index";
import { decodeURIFilter, encodeURIFilter, useRootPath } from "utils/uri";

import { landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";

const Container = styled.div`
display: flex;
flex-direction: column;
gap: ${responsiveSize(8, 16)};
${landscapeStyle(
() => css`
flex-direction: row;
`
)}
`;

const StyledSearchbar = styled(Searchbar)`
flex: 1;
flex-basis: 310px;
width: 100%;
input {
font-size: 16px;
@@ -61,15 +45,13 @@ const Search: React.FC = () => {
);

return (
<Container>
<StyledSearchbar
dir="auto"
type="text"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</Container>
<StyledSearchbar
dir="auto"
type="text"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
);
};

21 changes: 17 additions & 4 deletions web/src/pages/Jurors/Stats.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from "react";
import styled from "styled-components";

import Skeleton from "react-loading-skeleton";

import { isUndefined } from "utils/index";

const FieldWrapper = styled.div`
display: inline-flex;
gap: 8px;
@@ -10,19 +14,28 @@ const StyledLabel = styled.label`
color: ${({ theme }) => theme.primaryText};
`;

const Field: React.FC<{ label: string; value: string }> = ({ label, value }) => (
const ValueAndExtraLabel = styled.div`
display: flex;
gap: 4px;
`;

const Field: React.FC<{ label: string; value?: number; extraLabel?: string }> = ({ label, value, extraLabel }) => (
<FieldWrapper>
<StyledLabel>{label}</StyledLabel>
<small>{value}</small>
<ValueAndExtraLabel>
<small>{!isUndefined(value) ? value : <Skeleton width={16} />}</small>
{extraLabel ? <small>{extraLabel}</small> : null}
</ValueAndExtraLabel>
</FieldWrapper>
);

export interface IStats {
totalJurors: number;
totalJurors?: number;
}

const Stats: React.FC<IStats> = ({ totalJurors }) => {
return <Field label="Total" value={`${totalJurors} Jurors`} />;
const value = !isUndefined(totalJurors) ? totalJurors : undefined;
return <Field label="Total" value={value} extraLabel="Jurors" />;
};

export default Stats;
2 changes: 1 addition & 1 deletion web/src/pages/Jurors/StatsAndFilters.tsx
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ const Container = styled.div`
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: ${responsiveSize(4, 8)};
margin-top: ${responsiveSize(12, 13)};
margin-bottom: ${responsiveSize(16, 32)};
justify-content: space-between;
`;
63 changes: 35 additions & 28 deletions web/src/pages/Jurors/index.tsx
Original file line number Diff line number Diff line change
@@ -4,19 +4,20 @@ import styled, { css } from "styled-components";
import { MAX_WIDTH_LANDSCAPE, landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";

import Skeleton from "react-loading-skeleton";
import { useAccount } from "wagmi";

import { isUndefined } from "utils/index";
import { getOneYearAgoTimestamp } from "utils/date";

import { useTopUsersByCoherenceScore } from "queries/useTopUsersByCoherenceScore";
import { useHomePageQuery } from "queries/useHomePageQuery";
import { useTotalLeaderboardJurors } from "queries/useTotalLeaderboardJurors";

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

import { SkeletonDisputeListItem } from "components/StyledSkeleton";
import Search from "./Search";
import StatsAndFilters from "./StatsAndFilters";
import JurorCard from "../Home/TopJurors/JurorCard";
import { ListContainer, StyledLabel } from "../Home/TopJurors";
import Header from "../Home/TopJurors/Header";
import { getLastOrZero } from "../Home/CourtOverview/Stats";
import DisplayJurors from "./DisplayJurors";
import { StyledArrowLink } from "components/StyledArrowLink";
import ConnectWallet from "components/ConnectWallet";

const Container = styled.div`
width: 100%;
@@ -32,37 +33,43 @@ const Container = styled.div`
)}
`;

const Header = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: ${responsiveSize(12, 24)};
gap: 4px;
`;

const StyledTitle = styled.h1`
margin: 0px;
font-size: ${responsiveSize(20, 24)};
`;

const Jurors: React.FC = () => {
const { data: queryJurors } = useTopUsersByCoherenceScore(1000);

const jurors = queryJurors?.users?.map((juror, index) => ({
...juror,
rank: index + 1,
}));

// const { data } = useHomePageQuery(getOneYearAgoTimestamp());
// const totalActiveJurors = data && getLastOrZero(data["counters"], "activeJurors");
const { data: queryTotalLeaderBoardJurors } = useTotalLeaderboardJurors();
const totalLeaderboardJurors = queryTotalLeaderBoardJurors?.counter?.totalLeaderboardJurors;
const { isConnected } = useAccount();

return (
<Container>
<StyledTitle>Jurors Leaderboard</StyledTitle>
<Header>
<StyledTitle>Jurors Leaderboard</StyledTitle>
{isConnected ? (
<StyledArrowLink to={"/profile/1/desc/all"}>
My Profile <ArrowIcon />
</StyledArrowLink>
) : (
<ConnectWallet />
)}
</Header>
<Search />
<StatsAndFilters totalJurors={0} />

{!isUndefined(jurors) && jurors.length === 0 ? (
<StyledLabel>There are no jurors staked yet.</StyledLabel>
<StatsAndFilters totalJurors={totalLeaderboardJurors} />
{!isUndefined(totalLeaderboardJurors) && Number(totalLeaderboardJurors) > 0 ? (
<DisplayJurors totalLeaderboardJurors={Number(totalLeaderboardJurors)} />
) : (
<ListContainer>
<Header />
{!isUndefined(jurors)
? jurors.map((juror) => <JurorCard key={juror.rank} address={juror.id} {...juror} />)
: [...Array(14)].map((_, i) => <SkeletonDisputeListItem key={i} />)}
</ListContainer>
<Skeleton height={1000} />
)}
</Container>
);
7 changes: 3 additions & 4 deletions web/src/pages/Profile/JurorInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import Coherence from "./Coherence";
import Header from "./Header";
import JurorRewards from "./JurorRewards";
import PixelArt from "./PixelArt";
import { getPercent } from "pages/Home/TopJurors/JurorCard/Coherence";

const Container = styled.div``;

@@ -43,13 +44,11 @@ interface IJurorInfo {

const JurorInfo: React.FC<IJurorInfo> = ({ addressToQuery }) => {
const { data } = useUserQuery(addressToQuery);
// TODO check graph schema
const coherenceScore = data?.user ? parseInt(data?.user?.coherenceScore) : 0;
const totalCoherentVotes = data?.user ? parseInt(data?.user?.totalCoherentVotes) : 0;
const totalResolvedVotes = data?.user ? parseInt(data?.user?.totalResolvedVotes) : 0;
const totalResolvedDisputes = data?.user ? parseInt(data?.user?.totalResolvedDisputes) : 0;

const userLevelData = getUserLevelData(coherenceScore, totalResolvedDisputes);
const coherencePercentage = getPercent(Number(totalCoherentVotes), Number(totalResolvedVotes));
const userLevelData = getUserLevelData(coherencePercentage, totalResolvedDisputes);

return (
<Container>
23 changes: 13 additions & 10 deletions web/src/utils/userLevelCalculation.ts
Original file line number Diff line number Diff line change
@@ -2,24 +2,27 @@ interface ILevelCriteria {
level: number;
title: string;
minDisputes: number;
minScore: number;
maxScore: number;
minCoherencePercentage: number;
maxCoherencePercentage: number;
}

const levelCriteria: ILevelCriteria[] = [
{ level: 4, title: "Aristotle", minDisputes: 10, minScore: 91, maxScore: 100 },
{ level: 3, title: "Plato", minDisputes: 7, minScore: 81, maxScore: 90 },
{ level: 2, title: "Socrates", minDisputes: 3, minScore: 71, maxScore: 80 },
{ level: 1, title: "Pythagoras", minDisputes: 0, minScore: 0, maxScore: 70 },
{ level: 0, title: "Diogenes", minDisputes: 3, minScore: 0, maxScore: 49 },
{ level: 4, title: "Aristotle", minDisputes: 10, minCoherencePercentage: 90, maxCoherencePercentage: 100 },
{ level: 3, title: "Plato", minDisputes: 7, minCoherencePercentage: 80, maxCoherencePercentage: 100 },
{ level: 2, title: "Socrates", minDisputes: 3, minCoherencePercentage: 70, maxCoherencePercentage: 100 },
{ level: 1, title: "Pythagoras", minDisputes: 1, minCoherencePercentage: 0, maxCoherencePercentage: 70 },
{ level: 0, title: "Diogenes", minDisputes: 3, minCoherencePercentage: 0, maxCoherencePercentage: 49 },
];

export const getUserLevelData = (coherenceScore: number, totalResolvedDisputes: number) => {
export const getUserLevelData = (coherencePercentage: string, totalResolvedDisputes: number) => {
const percentageToNumber = (percentage: string) => parseFloat(percentage.replace("%", ""));
const coherenceValue = percentageToNumber(coherencePercentage);

for (const criteria of levelCriteria) {
if (
totalResolvedDisputes >= criteria.minDisputes &&
coherenceScore >= criteria.minScore &&
coherenceScore <= criteria.maxScore
coherenceValue >= criteria.minCoherencePercentage &&
coherenceValue <= criteria.maxCoherencePercentage
) {
return criteria;
}