Skip to content

Commit

Permalink
Merge pull request #1819 from kleros/feat(web)/public-dashboards
Browse files Browse the repository at this point in the history
feat: public dashboards
  • Loading branch information
alcercu authored Jan 7, 2025
2 parents 50bcfb9 + 8b11b62 commit 6d09c7d
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 76 deletions.
7 changes: 4 additions & 3 deletions web/src/components/CasesDisplay/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled, { css, useTheme } from "styled-components";

import { hoverShortTransitionTiming } from "styles/commonStyles";

import { useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";

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

Expand Down Expand Up @@ -55,16 +55,17 @@ const Filters: React.FC = () => {
const { ruled, period, ...filterObject } = decodeURIFilter(filter ?? "all");
const navigate = useNavigate();
const location = useRootPath();
const [searchParams] = useSearchParams();

const handleStatusChange = (value: string | number) => {
const parsedValue = JSON.parse(value as string);
const encodedFilter = encodeURIFilter({ ...filterObject, ...parsedValue });
navigate(`${location}/1/${order}/${encodedFilter}`);
navigate(`${location}/1/${order}/${encodedFilter}?${searchParams.toString()}`);
};

const handleOrderChange = (value: string | number) => {
const encodedFilter = encodeURIFilter({ ruled, period, ...filterObject });
navigate(`${location}/1/${value}/${encodedFilter}`);
navigate(`${location}/1/${value}/${encodedFilter}?${searchParams.toString()}`);
};

const { isList, setIsList } = useIsList();
Expand Down
19 changes: 14 additions & 5 deletions web/src/components/CasesDisplay/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useMemo, useState } from "react";
import React, { useMemo, useRef, useState } from "react";
import styled, { css } from "styled-components";

import Skeleton from "react-loading-skeleton";
import { useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useDebounce } from "react-use";

import { Searchbar, DropdownCascader } from "@kleros/ui-components-library";

import { isEmpty, isUndefined } from "utils/index";
Expand Down Expand Up @@ -39,6 +38,7 @@ const SearchBarContainer = styled.div`
const StyledSearchbar = styled(Searchbar)`
flex: 1;
flex-basis: 310px;
input {
font-size: 16px;
height: 45px;
Expand All @@ -53,16 +53,25 @@ const Search: React.FC = () => {
const decodedFilter = decodeURIFilter(filter ?? "all");
const { id: searchValue, ...filterObject } = decodedFilter;
const [search, setSearch] = useState(searchValue ?? "");
const initialRenderRef = useRef(true);
const navigate = useNavigate();
const [searchParams] = useSearchParams();

useDebounce(
() => {
if (initialRenderRef.current && isEmpty(search)) {
initialRenderRef.current = false;
return;
}
initialRenderRef.current = false;
const newFilters = isEmpty(search) ? { ...filterObject } : { ...filterObject, id: search };
const encodedFilter = encodeURIFilter(newFilters);
navigate(`${location}/${page}/${order}/${encodedFilter}`);
navigate(`${location}/${page}/${order}/${encodedFilter}?${searchParams.toString()}`);
},
500,
[search]
);

const { data: courtTreeData } = useCourtTree();
const items = useMemo(() => {
if (!isUndefined(courtTreeData)) {
Expand All @@ -82,7 +91,7 @@ const Search: React.FC = () => {
onSelect={(value) => {
const { court: _, ...filterWithoutCourt } = decodedFilter;
const newFilter = value === "all" ? filterWithoutCourt : { ...decodedFilter, court: value.toString() };
navigate(`${location}/${page}/${order}/${encodeURIFilter(newFilter)}`);
navigate(`${location}/${page}/${order}/${encodeURIFilter(newFilter)}?${searchParams.toString()}`);
}}
/>
) : (
Expand Down
17 changes: 9 additions & 8 deletions web/src/components/EvidenceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const AccountContainer = styled.div`
}
`;

const HoverStyle = css`
const ExternalLinkHoverStyle = css`
:hover {
text-decoration: underline;
color: ${({ theme }) => theme.primaryBlue};
Expand All @@ -155,12 +155,15 @@ const HoverStyle = css`
`;

const Address = styled.p`
${HoverStyle}
margin: 0;
:hover {
color: ${({ theme }) => theme.secondaryBlue};
}
`;

const StyledExternalLink = styled(ExternalLink)`
${HoverStyle}
${ExternalLinkHoverStyle}
`;

const DesktopText = styled.span`
Expand Down Expand Up @@ -221,9 +224,7 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
description,
fileURI,
}) => {
const addressExplorerLink = useMemo(() => {
return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/address/${sender}`;
}, [sender]);
const dashboardLink = `/dashboard/1/desc/all?address=${sender}`;

const transactionExplorerLink = useMemo(() => {
return getTxnExplorerLink(transactionHash ?? "");
Expand All @@ -248,9 +249,9 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
<BottomLeftContent>
<AccountContainer>
<Identicon size="24" string={sender} />
<ExternalLink to={addressExplorerLink} rel="noopener noreferrer" target="_blank">
<InternalLink to={dashboardLink}>
<Address>{shortenAddress(sender)}</Address>
</ExternalLink>
</InternalLink>
</AccountContainer>
<StyledExternalLink to={transactionExplorerLink} rel="noopener noreferrer" target="_blank">
<label>{formatDate(Number(timestamp), true)}</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React, { useMemo } from "react";
import React from "react";
import styled, { css } from "styled-components";

import { landscapeStyle } from "styles/landscapeStyle";

import Identicon from "react-identicons";

import { Answer } from "context/NewDisputeContext";
import { DEFAULT_CHAIN, getChain } from "consts/chains";
import { getVoteChoice } from "utils/getVoteChoice";
import { isUndefined } from "utils/index";
import { shortenAddress } from "utils/shortenAddress";

import { landscapeStyle } from "styles/landscapeStyle";
import { InternalLink } from "components/InternalLink";

const TitleContainer = styled.div`
display: flex;
Expand Down Expand Up @@ -41,12 +42,11 @@ const StyledSmall = styled.small`
font-size: 16px;
`;

const StyledA = styled.a`
const StyledInternalLink = styled(InternalLink)`
:hover {
text-decoration: underline;
label {
cursor: pointer;
color: ${({ theme }) => theme.primaryBlue};
color: ${({ theme }) => theme.secondaryBlue};
}
}
`;
Expand Down Expand Up @@ -88,17 +88,15 @@ const AccordionTitle: React.FC<{
commited: boolean;
hiddenVotes: boolean;
}> = ({ juror, choice, voteCount, period, answers, isActiveRound, commited, hiddenVotes }) => {
const addressExplorerLink = useMemo(() => {
return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/address/${juror}`;
}, [juror]);
const dashboardLink = `/dashboard/1/desc/all?address=${juror}`;

return (
<TitleContainer>
<AddressContainer>
<Identicon size="20" string={juror} />
<StyledA href={addressExplorerLink} rel="noopener noreferrer" target="_blank">
<StyledInternalLink to={dashboardLink}>
<StyledLabel variant="secondaryText">{shortenAddress(juror)}</StyledLabel>
</StyledA>
</StyledInternalLink>
</AddressContainer>
<VoteStatus {...{ choice, period, answers, isActiveRound, commited, hiddenVotes }} />
<StyledLabel variant="secondaryPurple">
Expand Down
5 changes: 4 additions & 1 deletion web/src/pages/Dashboard/Courts/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import styled, { css } from "styled-components";

import { formatUnits } from "viem";
import { useSearchParams } from "react-router-dom";

import LockerIcon from "svgs/icons/locker.svg";

Expand Down Expand Up @@ -57,10 +58,12 @@ interface IHeader {

const Header: React.FC<IHeader> = ({ lockedStake }) => {
const formattedLockedStake = !isUndefined(lockedStake) && formatUnits(lockedStake, 18);
const [searchParams] = useSearchParams();
const searchParamAddress = searchParams.get("address")?.toLowerCase();

return (
<Container>
<StyledTitle>My Courts</StyledTitle>
<StyledTitle>{searchParamAddress ? "Their" : "My"} Courts</StyledTitle>
{!isUndefined(lockedStake) ? (
<LockedPnk>
<StyledLockerIcon />
Expand Down
19 changes: 13 additions & 6 deletions web/src/pages/Dashboard/Courts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import styled, { css } from "styled-components";

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

import { useReadSortitionModuleGetJurorBalance } from "hooks/contracts/generated";

Expand Down Expand Up @@ -35,12 +35,17 @@ const StyledLabel = styled.label`
font-size: ${responsiveSize(14, 16)};
`;

const Courts: React.FC = () => {
const { address } = useAccount();
const { data: stakeData, isLoading } = useJurorStakeDetailsQuery(address?.toLowerCase() as `0x${string}`);
interface ICourts {
addressToQuery: `0x${string}`;
}

const Courts: React.FC<ICourts> = ({ addressToQuery }) => {
const { data: stakeData, isLoading } = useJurorStakeDetailsQuery(addressToQuery);
const { data: jurorBalance } = useReadSortitionModuleGetJurorBalance({
args: [address as `0x${string}`, BigInt(1)],
args: [addressToQuery, BigInt(1)],
});
const [searchParams] = useSearchParams();
const searchParamAddress = searchParams.get("address")?.toLowerCase();
const stakedCourts = stakeData?.jurorTokensPerCourts?.filter(({ staked }) => staked > 0);
const isStaked = stakedCourts && stakedCourts.length > 0;
const lockedStake = jurorBalance?.[1];
Expand All @@ -49,7 +54,9 @@ const Courts: React.FC = () => {
<Container>
<Header lockedStake={lockedStake ?? BigInt(0)} />
{isLoading ? <Skeleton /> : null}
{!isStaked && !isLoading ? <StyledLabel>You are not staked in any court</StyledLabel> : null}
{!isStaked && !isLoading ? (
<StyledLabel>{searchParamAddress ? "They" : "You"} are not staked in any court</StyledLabel>
) : null}
{isStaked && !isLoading ? (
<CourtCardsContainer>
{stakeData?.jurorTokensPerCourts
Expand Down
39 changes: 35 additions & 4 deletions web/src/pages/Dashboard/JurorInfo/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import React from "react";
import React, { useMemo } from "react";
import styled from "styled-components";

import { responsiveSize } from "styles/responsiveSize";

import { useToggle } from "react-use";
import { useSearchParams } from "react-router-dom";
import { Copiable } from "@kleros/ui-components-library";

import XIcon from "svgs/socialmedia/x.svg";

import { DEFAULT_CHAIN, getChain } from "consts/chains";
import { shortenAddress } from "utils/shortenAddress";

import HowItWorks from "components/HowItWorks";
import JurorLevels from "components/Popup/MiniGuides/JurorLevels";
import { ExternalLink } from "components/ExternalLink";
Expand Down Expand Up @@ -45,31 +50,57 @@ const StyledLink = styled(ExternalLink)`
gap: 8px;
`;

const StyledExternalLink = styled(ExternalLink)`
font-size: ${responsiveSize(18, 22)};
margin-left: ${responsiveSize(4, 8)};
font-weight: 600;
`;

interface IHeader {
levelTitle: string;
levelNumber: number;
totalCoherentVotes: number;
totalResolvedVotes: number;
addressToQuery: `0x${string}`;
}

const Header: React.FC<IHeader> = ({ levelTitle, levelNumber, totalCoherentVotes, totalResolvedVotes }) => {
const Header: React.FC<IHeader> = ({
levelTitle,
levelNumber,
totalCoherentVotes,
totalResolvedVotes,
addressToQuery,
}) => {
const [isJurorLevelsMiniGuideOpen, toggleJurorLevelsMiniGuide] = useToggle(false);
const [searchParams] = useSearchParams();

const coherencePercentage = parseFloat(((totalCoherentVotes / Math.max(totalResolvedVotes, 1)) * 100).toFixed(2));
const courtUrl = window.location.origin;
const xPostText = `Hey I've been busy as a Juror on the Kleros court, check out my score: \n\nLevel: ${levelNumber} (${levelTitle})\nCoherence Percentage: ${coherencePercentage}%\nCoherent Votes: ${totalCoherentVotes}/${totalResolvedVotes}\n\nBe a juror with me! ➡️ ${courtUrl}`;
const xShareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(xPostText)}`;
const searchParamAddress = searchParams.get("address")?.toLowerCase();

const addressExplorerLink = useMemo(() => {
return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/address/${addressToQuery}`;
}, [addressToQuery]);

return (
<Container>
<StyledTitle>Juror Dashboard</StyledTitle>
<StyledTitle>
Juror Dashboard -
<Copiable copiableContent={addressToQuery} info="Copy Address">
<StyledExternalLink to={addressExplorerLink} target="_blank" rel="noopener noreferrer">
{shortenAddress(addressToQuery)}
</StyledExternalLink>
</Copiable>
</StyledTitle>
<LinksContainer>
<HowItWorks
isMiniGuideOpen={isJurorLevelsMiniGuideOpen}
toggleMiniGuide={toggleJurorLevelsMiniGuide}
MiniGuideComponent={JurorLevels}
/>
{totalResolvedVotes > 0 ? (
{totalResolvedVotes > 0 && !searchParamAddress ? (
<StyledLink to={xShareUrl} target="_blank" rel="noreferrer">
<StyledXIcon /> <span>Share your juror score</span>
</StyledLink>
Expand Down
9 changes: 6 additions & 3 deletions web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ const tooltipMsg =
"is coherent with the final ruling receive the Juror Rewards composed of " +
"arbitration fees (ETH) + PNK redistribution between jurors.";

const JurorRewards: React.FC = () => {
const { address } = useAccount();
const { data } = useUserQuery(address?.toLowerCase() as `0x${string}`);
interface IJurorRewards {
addressToQuery: `0x${string}`;
}

const JurorRewards: React.FC<IJurorRewards> = ({ addressToQuery }) => {
const { data } = useUserQuery(addressToQuery);
const coinIds = [CoinIds.PNK, CoinIds.ETH];
const { prices: pricesData } = useCoinPrice(coinIds);

Expand Down
17 changes: 9 additions & 8 deletions web/src/pages/Dashboard/JurorInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React from "react";
import styled, { css } from "styled-components";

import { useAccount } from "wagmi";

import { Card as _Card } from "@kleros/ui-components-library";

import { getUserLevelData } from "utils/userLevelCalculation";
Expand Down Expand Up @@ -39,9 +37,12 @@ const Card = styled(_Card)`
)}
`;

const JurorInfo: React.FC = () => {
const { address } = useAccount();
const { data } = useUserQuery(address?.toLowerCase() as `0x${string}`);
interface IJurorInfo {
addressToQuery: `0x${string}`;
}

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;
Expand All @@ -55,12 +56,12 @@ const JurorInfo: React.FC = () => {
<Header
levelTitle={userLevelData.title}
levelNumber={userLevelData.level}
{...{ totalCoherentVotes, totalResolvedVotes }}
{...{ totalCoherentVotes, totalResolvedVotes, addressToQuery }}
/>
<Card>
<PixelArt level={userLevelData.level} width="189px" height="189px" />
<Coherence userLevelData={userLevelData} isMiniGuide={false} {...{ totalCoherentVotes, totalResolvedVotes }} />
<JurorRewards />
<Coherence isMiniGuide={false} {...{ userLevelData, totalCoherentVotes, totalResolvedVotes }} />
<JurorRewards {...{ addressToQuery }} />
</Card>
</Container>
);
Expand Down
Loading

0 comments on commit 6d09c7d

Please sign in to comment.