diff --git a/contracts/audit/METRICS.md b/contracts/audit/METRICS.md
index c0cce1621..65be14e5d 100644
--- a/contracts/audit/METRICS.md
+++ b/contracts/audit/METRICS.md
@@ -119,7 +119,6 @@ Total: 15
-
#### Out of Scope
##### Excluded Source Units
@@ -288,7 +287,6 @@ This section lists functions that are explicitly declared public or payable. Ple
-
Files Description Table
| File Name | SHA-1 Hash |
diff --git a/web/src/app.tsx b/web/src/app.tsx
index 83f82eac0..21f179c16 100644
--- a/web/src/app.tsx
+++ b/web/src/app.tsx
@@ -74,7 +74,7 @@ const App: React.FC = () => {
}
/>
}>
diff --git a/web/src/assets/svgs/icons/voted-ballot.svg b/web/src/assets/svgs/icons/voted-ballot.svg
new file mode 100644
index 000000000..3b9bb7c04
--- /dev/null
+++ b/web/src/assets/svgs/icons/voted-ballot.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/web/src/components/DisputeView/PeriodBanner.tsx b/web/src/components/DisputeView/PeriodBanner.tsx
index 6a5287afa..1a120e1c6 100644
--- a/web/src/components/DisputeView/PeriodBanner.tsx
+++ b/web/src/components/DisputeView/PeriodBanner.tsx
@@ -58,13 +58,14 @@ const StyledLabel = styled.label<{ frontColor: string; withDot?: boolean; isCard
`
: null}
`;
+
export interface IPeriodBanner {
id: number;
period: Periods;
isCard?: boolean;
}
-const getPeriodColors = (period: Periods, theme: Theme): [string, string] => {
+export const getPeriodColors = (period: Periods, theme: Theme): [string, string] => {
switch (period) {
case Periods.appeal:
return [theme.tint, theme.tintMedium];
diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx
index 90ea42f00..99f6d7ffc 100644
--- a/web/src/components/EvidenceCard.tsx
+++ b/web/src/components/EvidenceCard.tsx
@@ -17,10 +17,9 @@ import { hoverShortTransitionTiming } from "styles/commonStyles";
import { landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";
-import JurorTitle from "pages/Home/TopJurors/JurorCard/JurorTitle";
-
import { ExternalLink } from "./ExternalLink";
import { InternalLink } from "./InternalLink";
+import JurorLink from "components/JurorLink";
import MarkdownRenderer from "./MarkdownRenderer";
const StyledCard = styled(Card)`
@@ -201,8 +200,8 @@ const EvidenceCard: React.FC
= ({
description,
fileURI,
}) => {
- const profileLink = `/profile/1/desc/all?address=${sender}`;
const { id } = useParams();
+ const profileLink = `/profile/stakes/1?address=${sender}`;
const transactionExplorerLink = useMemo(() => {
return getTxnExplorerLink(transactionHash ?? "");
@@ -228,7 +227,7 @@ const EvidenceCard: React.FC = ({
-
+
{formatDate(Number(timestamp), true)}
diff --git a/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx b/web/src/components/JurorLink.tsx
similarity index 55%
rename from web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx
rename to web/src/components/JurorLink.tsx
index 4eb87f0c1..de81556e3 100644
--- a/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx
+++ b/web/src/components/JurorLink.tsx
@@ -1,11 +1,15 @@
-import React from "react";
+import React, { useMemo } from "react";
import styled from "styled-components";
-import ArrowSvg from "svgs/icons/arrow.svg";
+import { useAccount } from "wagmi";
+
+import ArrowIcon from "svgs/icons/arrow.svg";
+import NewTabIcon from "svgs/icons/new-tab.svg";
+
+import { DEFAULT_CHAIN, getChain } from "consts/chains";
import { IdenticonOrAvatar, AddressOrName } from "components/ConnectWallet/AccountDisplay";
import { StyledArrowLink } from "components/StyledArrowLink";
-import { useAccount } from "wagmi";
const Container = styled.div`
display: flex;
@@ -45,27 +49,36 @@ export const ReStyledArrowLink = styled(StyledArrowLink)<{ smallDisplay?: boolea
`}
`;
-interface IJurorTitle {
+interface IJurorLink {
address: string;
+ isInternalLink?: boolean;
smallDisplay?: boolean;
}
-const JurorTitle: React.FC = ({ address, smallDisplay }) => {
+const JurorLink: React.FC = ({ address, isInternalLink = true, smallDisplay }) => {
const { isConnected, address: connectedAddress } = useAccount();
const profileLink =
isConnected && connectedAddress?.toLowerCase() === address.toLowerCase()
- ? "/profile/1/desc/all"
- : `/profile/1/desc/all?address=${address}`;
+ ? "/profile"
+ : `/profile/stakes/1?address=${address}`;
+ const addressExplorerLink = useMemo(() => {
+ return `${getChain(DEFAULT_CHAIN)?.blockExplorers?.default.url}/address/${address}`;
+ }, [address]);
return (
-
-
+
+
-
+ {isInternalLink ? : }
);
};
-export default JurorTitle;
+export default JurorLink;
diff --git a/web/src/components/Popup/MiniGuides/JurorLevels.tsx b/web/src/components/Popup/MiniGuides/JurorLevels.tsx
index a250cc355..eb05292d0 100644
--- a/web/src/components/Popup/MiniGuides/JurorLevels.tsx
+++ b/web/src/components/Popup/MiniGuides/JurorLevels.tsx
@@ -5,8 +5,8 @@ import { Card as _Card } from "@kleros/ui-components-library";
import { landscapeStyle } from "styles/landscapeStyle";
-import Coherence from "pages/Profile/JurorInfo/Coherence";
-import PixelArt from "pages/Profile/JurorInfo/PixelArt";
+import Coherence from "pages/Profile/JurorCard/BottomContent/Coherence";
+import PixelArt from "pages/Profile/JurorCard/BottomContent/PixelArt";
import Template from "./MainStructureTemplate";
import { Title, ParagraphsContainer, LeftContentContainer } from "./PageContentsTemplate";
diff --git a/web/src/hooks/queries/useStakingHistory.ts b/web/src/hooks/queries/useStakingHistory.ts
new file mode 100644
index 000000000..126c41bd1
--- /dev/null
+++ b/web/src/hooks/queries/useStakingHistory.ts
@@ -0,0 +1,62 @@
+import { useQuery } from "@tanstack/react-query";
+
+// dynamic atlasUri would go here
+const atlasUri = "https://url.example/graphql";
+
+const AUTH_TOKEN = "Bearer tokenExampleGoesHere";
+
+export const useStakingHistory = (take: number, lastCursorId?: number) => {
+ const variables = {
+ pagination: { take, lastCursorId: lastCursorId ?? null },
+ };
+
+ return useQuery({
+ queryKey: ["stakingHistoryQuery", take, lastCursorId],
+ enabled: true,
+ staleTime: 60000,
+ queryFn: async () => {
+ console.log("Fetching with variables:", variables);
+
+ try {
+ const response = await fetch(atlasUri, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: AUTH_TOKEN,
+ },
+ body: JSON.stringify({
+ query: `
+ query GetStakingEvents($pagination: PaginationArgs) {
+ userStakingEvents(pagination: $pagination) {
+ edges {
+ node {
+ name
+ args
+ blockTimestamp
+ transactionHash
+ }
+ cursor
+ }
+ count
+ hasNextPage
+ }
+ }
+ `,
+ variables,
+ }),
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error(`GraphQL error: ${JSON.stringify(result)}`);
+ }
+
+ return result;
+ } catch (error) {
+ console.error("GraphQL Fetch Error:", error);
+ throw error;
+ }
+ },
+ });
+};
diff --git a/web/src/hooks/useStakingEventsByCourt.ts b/web/src/hooks/useStakingEventsByCourt.ts
new file mode 100644
index 000000000..e8a2ec313
--- /dev/null
+++ b/web/src/hooks/useStakingEventsByCourt.ts
@@ -0,0 +1,74 @@
+import { useQuery } from "@tanstack/react-query";
+import { request } from "graphql-request";
+
+import { isUndefined } from "src/utils";
+
+export type StakingEventItem = {
+ id: string;
+ blockHash: string;
+ transactionHash: string;
+ blockTimestamp: string;
+ network: {
+ chainId: number;
+ };
+ args: {
+ _address: string;
+ _courtID: string;
+ _amount: string;
+ };
+};
+
+type StakingEventsResponse = {
+ userStakingEvents: {
+ items: Array<{ item: StakingEventItem }>;
+ count: number;
+ };
+};
+
+const atlasUri = import.meta.env.REACT_APP_ATLAS_URI;
+
+export const useStakingEventsByCourt = (courtIds: number[], skip: number, take: number, partialAddress?: string) => {
+ const addressParam = partialAddress ?? "";
+ // Allow empty courtIds array for "all courts" query
+ const isEnabled = !isUndefined(atlasUri);
+
+ const query = `
+ query GetStakingEvents($partialAddress: String!, $courtIDs: [Int!], $pagination: PaginationArgs) {
+ userStakingEvents(partialAddress: $partialAddress, courtIDs: $courtIDs, pagination: $pagination) {
+ items {
+ item {
+ id
+ blockHash
+ transactionHash
+ blockTimestamp
+ network {
+ chainId
+ }
+ args {
+ _address
+ _courtID
+ _amount
+ }
+ }
+ }
+ count
+ }
+ }
+ `;
+
+ const variables = {
+ partialAddress: addressParam,
+ // If courtIds is empty, pass null to query all courts
+ courtIDs: courtIds.length > 0 ? courtIds : null,
+ pagination: { skip, take },
+ };
+
+ return useQuery({
+ queryKey: ["stakingEventsByCourt", courtIds, skip, take, partialAddress],
+ enabled: isEnabled,
+ staleTime: 60000,
+ queryFn: async () => {
+ return await request(`${atlasUri}/graphql`, query, variables);
+ },
+ });
+};
diff --git a/web/src/layout/Header/navbar/Menu/Settings/General/WalletAndProfile.tsx b/web/src/layout/Header/navbar/Menu/Settings/General/WalletAndProfile.tsx
index 57931035c..bf7f0e5b5 100644
--- a/web/src/layout/Header/navbar/Menu/Settings/General/WalletAndProfile.tsx
+++ b/web/src/layout/Header/navbar/Menu/Settings/General/WalletAndProfile.tsx
@@ -52,7 +52,7 @@ const WalletAndProfile: React.FC = ({ toggleIsSettingsOpen }) => {
-
+
My Profile
diff --git a/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/AccordionTitle.tsx b/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/AccordionTitle.tsx
index 6c208351c..6d6de16c3 100644
--- a/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/AccordionTitle.tsx
+++ b/web/src/pages/Cases/CaseDetails/Voting/VotesDetails/AccordionTitle.tsx
@@ -8,7 +8,7 @@ import { getVoteChoice } from "utils/getVoteChoice";
import { isUndefined } from "utils/index";
import { InternalLink } from "components/InternalLink";
-import JurorTitle from "pages/Home/TopJurors/JurorCard/JurorTitle";
+import JurorLink from "components/JurorLink";
const TitleContainer = styled.div`
display: flex;
@@ -86,13 +86,13 @@ const AccordionTitle: React.FC<{
commited: boolean;
hiddenVotes: boolean;
}> = ({ juror, choice, voteCount, period, answers, isActiveRound, commited, hiddenVotes }) => {
- const profileLink = `/profile/1/desc/all?address=${juror}`;
+ const profileLink = `/profile/stakes/1?address=${juror}`;
return (
-
+
diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Header.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Header.tsx
index 01ebc8dc0..9f27d540c 100644
--- a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Header.tsx
+++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Header.tsx
@@ -10,7 +10,7 @@ const Container = styled.div`
border: 1px solid ${({ theme }) => theme.stroke};
border-top-left-radius: 3px;
border-top-right-radius: 3px;
- padding: 18px 24px;
+ padding: 16px 20px;
justify-content: space-between;
margin-top: ${responsiveSize(12, 16)};
`;
diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/JurorCard.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/JurorCard.tsx
index dafa1849e..394342ada 100644
--- a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/JurorCard.tsx
+++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/JurorCard.tsx
@@ -3,7 +3,8 @@ import styled from "styled-components";
import { hoverShortTransitionTiming } from "styles/commonStyles";
-import JurorTitle from "pages/Home/TopJurors/JurorCard/JurorTitle";
+import JurorLink from "components/JurorLink";
+
import Stake from "./Stake";
const Container = styled.div`
@@ -15,7 +16,7 @@ const Container = styled.div`
border: 1px solid ${({ theme }) => theme.stroke};
border-top: none;
align-items: center;
- padding: 18px 24px;
+ padding: 16px 20px;
:hover {
background-color: ${({ theme }) => theme.lightGrey}BB;
@@ -30,7 +31,7 @@ interface IJurorCard {
const JurorCard: React.FC = ({ address, effectiveStake }) => {
return (
-
+
);
diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/index.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/index.tsx
index 8abc6e816..32d5d5143 100644
--- a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/index.tsx
+++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/index.tsx
@@ -1,15 +1,14 @@
import React from "react";
import styled from "styled-components";
-import { responsiveSize } from "styles/responsiveSize";
-
import { getDescriptiveCourtName } from "utils/getDescriptiveCourtName";
-import Search from "./Search";
+import { responsiveSize } from "styles/responsiveSize";
+
import DisplayJurors from "./DisplayJurors";
+import Search from "./Search";
const Container = styled.div`
- margin-top: ${responsiveSize(28, 48)};
max-width: 578px;
`;
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header.tsx
new file mode 100644
index 000000000..30cbde510
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header.tsx
@@ -0,0 +1,9 @@
+import React from "react";
+
+import { DesktopHeader } from "./Header/DesktopHeader";
+
+const Header: React.FC = () => {
+ return ;
+};
+
+export default Header;
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header/DesktopHeader.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header/DesktopHeader.tsx
new file mode 100644
index 000000000..1836be1c7
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header/DesktopHeader.tsx
@@ -0,0 +1,78 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { landscapeStyle } from "styles/landscapeStyle";
+import { responsiveSize } from "styles/responsiveSize";
+
+import WithHelpTooltip from "components/WithHelpTooltip";
+
+const Container = styled.div`
+ display: none;
+ width: 100%;
+ background-color: ${({ theme }) => theme.lightBlue};
+ border: 1px solid ${({ theme }) => theme.stroke};
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ padding: 16px 20px;
+ margin-top: ${responsiveSize(12, 16)};
+ gap: 12px;
+
+ ${landscapeStyle(
+ () => css`
+ display: flex;
+ `
+ )}
+`;
+
+const StyledLabel = styled.label`
+ font-size: 14px;
+ color: ${({ theme }) => theme.secondaryText};
+`;
+
+const JurorLabel = styled(StyledLabel)`
+ flex: 1;
+ min-width: 150px;
+ text-align: left;
+`;
+
+const StakeLabel = styled(StyledLabel)`
+ width: 90px;
+ text-align: right;
+ flex-shrink: 0;
+`;
+
+const CourtLabelContainer = styled.div`
+ width: 110px;
+ flex-shrink: 0;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+`;
+
+const CourtLabel = styled(StyledLabel)`
+ text-align: right;
+`;
+
+const DateLabel = styled(StyledLabel)`
+ width: 120px;
+ text-align: right;
+ flex-shrink: 0;
+`;
+
+const courtTooltipMsg =
+ "When you are staked in a subcourt you are staked in its parent courts too. eg. Staking in the Non Technical court automatically stake in its parents: Blockchain, and General Court.";
+
+export const DesktopHeader: React.FC = () => {
+ return (
+
+ Juror
+ PNK Staked
+
+
+ Court
+
+
+ Date
+
+ );
+};
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header/MobileHeader.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header/MobileHeader.tsx
new file mode 100644
index 000000000..c5caa148e
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/Header/MobileHeader.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { landscapeStyle } from "styles/landscapeStyle";
+import { responsiveSize } from "styles/responsiveSize";
+
+const Container = styled.div`
+ display: flex;
+ width: 100%;
+ background-color: ${({ theme }) => theme.lightBlue};
+ border: 1px solid ${({ theme }) => theme.stroke};
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ padding: 16px;
+ margin-top: ${responsiveSize(12, 16)};
+
+ ${landscapeStyle(
+ () => css`
+ display: none;
+ `
+ )}
+`;
+
+const StyledLabel = styled.label`
+ font-size: 14px;
+ color: ${({ theme }) => theme.secondaryText};
+`;
+
+export const MobileHeader: React.FC = () => {
+ return (
+
+ Staking History
+
+ );
+};
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard.tsx
new file mode 100644
index 000000000..1eed9f5f5
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+
+import DesktopCard from "./StakeEventCard/DesktopCard";
+import MobileCard from "./StakeEventCard/MobileCard";
+
+interface IStakeEventCard {
+ address: string;
+ stake: string;
+ timestamp: string;
+ transactionHash: string;
+ courtName: string;
+ courtId: number;
+ currentCourtId?: number;
+}
+
+const StakeEventCard: React.FC = ({
+ address,
+ stake,
+ timestamp,
+ transactionHash,
+ courtName,
+ courtId,
+ currentCourtId,
+}) => {
+ const allProps = { address, stake, timestamp, transactionHash, courtName, courtId, currentCourtId };
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default StakeEventCard;
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard/DesktopCard.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard/DesktopCard.tsx
new file mode 100644
index 000000000..f775240ca
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard/DesktopCard.tsx
@@ -0,0 +1,153 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { formatPNK } from "utils/format";
+import { getTxnExplorerLink } from "utils/index";
+
+import { hoverShortTransitionTiming } from "styles/commonStyles";
+import { landscapeStyle } from "styles/landscapeStyle";
+
+import { InternalLink } from "components/InternalLink";
+import JurorLink from "components/JurorLink";
+
+const Container = styled.div`
+ ${hoverShortTransitionTiming}
+ display: none;
+ width: 100%;
+ min-width: 100%;
+ background-color: ${({ theme }) => theme.whiteBackground};
+ border: 1px solid ${({ theme }) => theme.stroke};
+ border-top: none;
+ align-items: center;
+ padding: 16px 20px;
+ gap: 12px;
+
+ ${landscapeStyle(
+ () => css`
+ display: flex;
+ `
+ )}
+
+ :hover {
+ background-color: ${({ theme }) => theme.lightGrey}BB;
+ }
+`;
+
+const JurorContainer = styled.div`
+ flex: 1;
+ min-width: 150px;
+ overflow: hidden;
+`;
+
+const StyledLabel = styled.label`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ flex-shrink: 0;
+`;
+
+const StakeLabel = styled(StyledLabel)`
+ width: 90px;
+ text-align: right;
+`;
+
+const CourtLabelContainer = styled.div`
+ width: 110px;
+ text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-shrink: 0;
+`;
+
+const CourtLink = styled(InternalLink)`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ cursor: pointer;
+ text-decoration: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const CourtText = styled.span`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const DateLabelContainer = styled.div`
+ width: 120px;
+ text-align: right;
+ flex-shrink: 0;
+`;
+
+const DateLink = styled.a`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ cursor: pointer;
+ text-decoration: none;
+
+ :hover {
+ text-decoration: underline;
+ }
+`;
+
+interface IDesktopCard {
+ address: string;
+ stake: string;
+ timestamp: string;
+ transactionHash: string;
+ courtName: string;
+ courtId: number;
+ currentCourtId?: number;
+}
+
+const formatDate = (timestamp: string): string => {
+ const date = new Date(parseInt(timestamp) * 1000);
+ return date.toLocaleDateString("en-GB", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+};
+
+const DesktopCard: React.FC = ({
+ address,
+ stake,
+ timestamp,
+ transactionHash,
+ courtName,
+ courtId,
+ currentCourtId,
+}) => {
+ const isCurrentCourt = currentCourtId === courtId;
+
+ return (
+
+
+
+
+ {formatPNK(BigInt(stake))}
+
+ {isCurrentCourt ? (
+ {courtName}
+ ) : (
+
+ {courtName}
+
+ )}
+
+
+
+ {formatDate(timestamp)}
+
+
+
+ );
+};
+
+export default DesktopCard;
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard/MobileCard.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard/MobileCard.tsx
new file mode 100644
index 000000000..bd99621e1
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/StakeEventCard/MobileCard.tsx
@@ -0,0 +1,148 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { formatPNK } from "utils/format";
+import { getTxnExplorerLink } from "utils/index";
+
+import { hoverShortTransitionTiming } from "styles/commonStyles";
+import { landscapeStyle } from "styles/landscapeStyle";
+
+import { InternalLink } from "components/InternalLink";
+import JurorLink from "components/JurorLink";
+
+const Container = styled.div`
+ ${hoverShortTransitionTiming}
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ background-color: ${({ theme }) => theme.whiteBackground};
+ border: 1px solid ${({ theme }) => theme.stroke};
+ border-top: none;
+ padding: 12px 16px;
+ gap: 12px;
+
+ ${landscapeStyle(
+ () => css`
+ display: none;
+ `
+ )}
+
+ :hover {
+ background-color: ${({ theme }) => theme.lightGrey}BB;
+ }
+`;
+
+const Row = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+`;
+
+const Label = styled.span`
+ font-size: 12px;
+ color: ${({ theme }) => theme.secondaryText};
+ font-weight: 400;
+`;
+
+const Value = styled.span`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ font-weight: 400;
+ text-align: right;
+`;
+
+const CourtLink = styled(InternalLink)`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ cursor: pointer;
+ text-decoration: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 200px;
+ text-align: right;
+`;
+
+const CourtText = styled.span`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 200px;
+ text-align: right;
+`;
+
+const DateLink = styled.a`
+ font-size: 14px;
+ color: ${({ theme }) => theme.primaryText};
+ cursor: pointer;
+ text-decoration: none;
+ text-align: right;
+
+ :hover {
+ text-decoration: underline;
+ }
+`;
+
+interface IMobileCard {
+ address: string;
+ stake: string;
+ timestamp: string;
+ transactionHash: string;
+ courtName: string;
+ courtId: number;
+ currentCourtId?: number;
+}
+
+const formatDate = (timestamp: string): string => {
+ const date = new Date(parseInt(timestamp) * 1000);
+ return date.toLocaleDateString("en-GB", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+};
+
+const MobileCard: React.FC = ({
+ address,
+ stake,
+ timestamp,
+ transactionHash,
+ courtName,
+ courtId,
+ currentCourtId,
+}) => {
+ const isCurrentCourt = currentCourtId === courtId;
+
+ return (
+
+
+
+ PNK Staked
+ {formatPNK(BigInt(stake))}
+
+
+ Court
+ {isCurrentCourt ? (
+ {courtName}
+ ) : (
+
+ {courtName}
+
+ )}
+
+
+ Date
+
+ {formatDate(timestamp)}
+
+
+
+ );
+};
+
+export default MobileCard;
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/index.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/index.tsx
new file mode 100644
index 000000000..486635f67
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/DisplayStakes/index.tsx
@@ -0,0 +1,186 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import styled from "styled-components";
+
+import { useParams, useSearchParams } from "react-router-dom";
+
+import { useStakingEventsByCourt } from "hooks/useStakingEventsByCourt";
+import { findCourtNameById } from "utils/findCourtNameById";
+import { isUndefined } from "utils/index";
+
+import { useCourtTree, CourtTreeQuery } from "queries/useCourtTree";
+
+import { responsiveSize } from "styles/responsiveSize";
+
+import { SkeletonDisputeListItem } from "components/StyledSkeleton";
+import { ListContainer as BaseListContainer } from "pages/Home/TopJurors";
+
+import Header from "./Header";
+import StakeEventCard from "./StakeEventCard";
+
+const ListContainer = styled(BaseListContainer)`
+ overflow: visible;
+`;
+
+const CardsWrapper = styled.div`
+ max-height: 520px;
+ overflow-y: hidden;
+
+ &:hover {
+ overflow-y: auto;
+ }
+`;
+
+const StyledLabel = styled.label`
+ display: flex;
+ font-size: 16px;
+ margin-top: ${responsiveSize(12, 20)};
+`;
+
+const PER_PAGE = 30;
+
+const getAllChildCourtIds = (court: CourtTreeQuery["court"], courtId: string): number[] => {
+ if (!court) return [];
+
+ const ids: number[] = [];
+
+ const traverse = (node: CourtTreeQuery["court"]) => {
+ if (!node) return;
+ if (node.id === courtId) {
+ ids.push(parseInt(node.id));
+ if (node.children) {
+ node.children.forEach((child) => {
+ ids.push(parseInt(child.id));
+ if (child.children) {
+ child.children.forEach((gc) => {
+ ids.push(parseInt(gc.id));
+ if (gc.children) {
+ gc.children.forEach((ggc) => {
+ ids.push(parseInt(ggc.id));
+ if (ggc.children) {
+ ggc.children.forEach((gggc) => ids.push(parseInt(gggc.id)));
+ }
+ });
+ }
+ });
+ }
+ });
+ }
+ return;
+ }
+ if (node.children) {
+ node.children.forEach((child) => traverse(child));
+ }
+ };
+
+ traverse(court);
+ return ids;
+};
+
+const DisplayStakes: React.FC = () => {
+ const { id: courtId } = useParams();
+ const [searchParams] = useSearchParams();
+ const searchValue = searchParams.get("stakeSearch") ?? "";
+ const [page, setPage] = useState(0);
+ const skip = page * PER_PAGE;
+
+ const { data: courtTree } = useCourtTree();
+
+ const courtIds = useMemo(() => {
+ if (!courtId || !courtTree?.court) return [];
+ return getAllChildCourtIds(courtTree.court, courtId);
+ }, [courtId, courtTree]);
+
+ const { data, isFetching } = useStakingEventsByCourt(courtIds, skip, PER_PAGE, searchValue || undefined);
+
+ const [acc, setAcc] = useState<
+ Array<{
+ id: string;
+ address: string;
+ stake: string;
+ timestamp: string;
+ transactionHash: string;
+ courtId: number;
+ courtName: string;
+ }>
+ >([]);
+
+ useEffect(() => {
+ setPage(0);
+ setAcc([]);
+ }, [searchValue, courtId]);
+
+ useEffect(() => {
+ const allItems = data?.userStakingEvents?.items ?? [];
+
+ const filteredItems = allItems.filter((item) => {
+ const itemCourtId = item.item.args._courtID;
+ return courtIds.includes(parseInt(itemCourtId));
+ });
+
+ const chunk = filteredItems.map((item) => {
+ const itemCourtId = item.item.args._courtID;
+ const courtName = findCourtNameById(courtTree, itemCourtId);
+
+ return {
+ id: item.item.id,
+ address: item.item.args._address,
+ stake: item.item.args._amount,
+ timestamp: item.item.blockTimestamp,
+ transactionHash: item.item.transactionHash,
+ courtId: parseInt(itemCourtId),
+ courtName: courtName ?? `Court #${itemCourtId}`,
+ };
+ });
+
+ const sortedChunk = chunk.sort((a, b) => parseInt(b.timestamp) - parseInt(a.timestamp));
+
+ if (sortedChunk.length) setAcc((prev) => [...prev, ...sortedChunk]);
+ }, [data, courtIds, courtTree]);
+
+ const sentinelRef = useRef(null);
+
+ useEffect(() => {
+ const sentinel = sentinelRef.current;
+ if (!sentinel) return;
+ const obs = new IntersectionObserver(
+ ([e]) => {
+ if (e.isIntersecting && !isFetching && acc.length % PER_PAGE === 0) setPage((p) => p + 1);
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(sentinel);
+ return () => obs.disconnect();
+ }, [isFetching, acc.length]);
+
+ const stakes = useMemo(() => acc, [acc]);
+
+ return (
+ <>
+ {!isUndefined(stakes) && stakes.length === 0 && !isFetching ? (
+ No stakes found
+ ) : (
+
+
+
+ {stakes.map((s) => (
+
+ ))}
+ {isFetching && [...Array(9)].map((_, i) => )}
+
+
+
+ )}
+ >
+ );
+};
+
+export default DisplayStakes;
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/Search.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/Search.tsx
new file mode 100644
index 000000000..9061ec664
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/Search.tsx
@@ -0,0 +1,63 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+
+import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
+import { useDebounce } from "react-use";
+
+import { Searchbar } from "@kleros/ui-components-library";
+
+import { isEmpty } from "utils/index";
+
+const Container = styled.div`
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 5px;
+ z-index: 0;
+`;
+
+const StyledSearchbar = styled(Searchbar)`
+ flex: 1;
+ flex-basis: 310px;
+ input {
+ font-size: 16px;
+ height: 45px;
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+`;
+
+const Search: React.FC = () => {
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+ const initial = searchParams.get("stakeSearch") ?? "";
+ const [value, setValue] = useState(initial);
+ useDebounce(
+ () => {
+ const params = new URLSearchParams(searchParams);
+ if (isEmpty(value)) {
+ params.delete("stakeSearch");
+ } else {
+ params.set("stakeSearch", value);
+ }
+ navigate(`${pathname}?${params.toString()}`, { replace: true });
+ },
+ 500,
+ [value]
+ );
+ return (
+
+ setValue(e.target.value)}
+ />
+
+ );
+};
+
+export default Search;
diff --git a/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/index.tsx b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/index.tsx
new file mode 100644
index 000000000..c812957ba
--- /dev/null
+++ b/web/src/pages/Courts/CourtDetails/StakingHistoryByCourt/index.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import styled from "styled-components";
+
+import { getDescriptiveCourtName } from "utils/getDescriptiveCourtName";
+
+import { responsiveSize } from "styles/responsiveSize";
+
+import DisplayStakes from "./DisplayStakes";
+import Search from "./Search";
+
+const Container = styled.div`
+ max-width: 578px;
+`;
+
+const Title = styled.h1`
+ margin-bottom: ${responsiveSize(12, 16)};
+ font-size: ${responsiveSize(20, 24)};
+`;
+
+const StakingHistoryByCourt: React.FC<{ courtName: string | undefined }> = ({ courtName }) => {
+ return (
+
+ Staking History in {getDescriptiveCourtName(courtName)}
+
+
+
+ );
+};
+
+export default StakingHistoryByCourt;
diff --git a/web/src/pages/Courts/CourtDetails/index.tsx b/web/src/pages/Courts/CourtDetails/index.tsx
index 448988711..3e8b05e97 100644
--- a/web/src/pages/Courts/CourtDetails/index.tsx
+++ b/web/src/pages/Courts/CourtDetails/index.tsx
@@ -7,7 +7,6 @@ import { useToggle } from "react-use";
import { Card, Breadcrumb } from "@kleros/ui-components-library";
import { isProductionDeployment } from "consts/index";
-
import { getDescriptiveCourtName } from "utils/getDescriptiveCourtName";
import { useCourtTree, CourtTreeQuery } from "queries/useCourtTree";
@@ -24,10 +23,11 @@ import ScrollTop from "components/ScrollTop";
import { StyledSkeleton } from "components/StyledSkeleton";
import Description from "./Description";
+import JurorsStakedByCourt from "./JurorsStakedByCourt";
import StakePanel from "./StakePanel";
+import StakingHistoryByCourt from "./StakingHistoryByCourt";
import Stats from "./Stats";
import TopSearch from "./TopSearch";
-import JurorsStakedByCourt from "./JurorsStakedByCourt";
const Container = styled.div``;
@@ -99,6 +99,23 @@ const StakePanelAndStats = styled.div`
)}
`;
+const StakingSections = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ margin-top: ${responsiveSize(28, 48)};
+
+ ${landscapeStyle(
+ () => css`
+ flex-direction: row;
+ gap: 48px;
+ & > * {
+ flex: 1 1 calc(50% - 24px);
+ }
+ `
+ )}
+`;
+
const CourtDetails: React.FC = () => {
const { id } = useParams();
const { data } = useCourtTree();
@@ -150,7 +167,10 @@ const CourtDetails: React.FC = () => {
-
+
+
+
+
);
diff --git a/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx b/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx
index e17c32b03..134b1965a 100644
--- a/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx
+++ b/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx
@@ -1,13 +1,14 @@
import React from "react";
import styled, { css } from "styled-components";
+import { hoverShortTransitionTiming } from "styles/commonStyles";
import { landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";
-import { hoverShortTransitionTiming } from "styles/commonStyles";
+
+import JurorLink from "components/JurorLink";
import Coherence from "./Coherence";
import JurorLevel from "./JurorLevel";
-import JurorTitle from "./JurorTitle";
import Rank from "./Rank";
import Rewards from "./Rewards";
import Score from "./Score";
@@ -58,7 +59,7 @@ const DesktopCard: React.FC = ({
return (
{renderRank && }
-
+
diff --git a/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx b/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx
index c961b8e61..8a2d104a4 100644
--- a/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx
+++ b/web/src/pages/Home/TopJurors/JurorCard/JurorLevel.tsx
@@ -5,7 +5,7 @@ import { landscapeStyle } from "styles/landscapeStyle";
import { getUserLevelData } from "utils/userLevelCalculation";
-import PixelArt from "pages/Profile/JurorInfo/PixelArt";
+import PixelArt from "pages/Profile/JurorCard/BottomContent/PixelArt";
const Container = styled.div`
display: flex;
diff --git a/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx b/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx
index c748b505b..c84ccb5bd 100644
--- a/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx
+++ b/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx
@@ -10,9 +10,9 @@ import HeaderRewards from "../Header/Rewards";
import Coherence from "./Coherence";
import JurorLevel from "./JurorLevel";
-import JurorTitle from "./JurorTitle";
import Rank from "./Rank";
import Rewards from "./Rewards";
+import JurorLink from "components/JurorLink";
import Score from "./Score";
const Container = styled.div`
@@ -107,7 +107,7 @@ const MobileCard: React.FC = ({
{rank ? : null}
-
+
diff --git a/web/src/pages/Jurors/index.tsx b/web/src/pages/Jurors/index.tsx
index 2d7666521..2b5fcc0fc 100644
--- a/web/src/pages/Jurors/index.tsx
+++ b/web/src/pages/Jurors/index.tsx
@@ -55,7 +55,7 @@ const Jurors: React.FC = () => {
Jurors Leaderboard
{isConnected ? (
-
+
My Profile
) : null}
diff --git a/web/src/pages/Profile/Cases/index.tsx b/web/src/pages/Profile/Cases/index.tsx
new file mode 100644
index 000000000..52e40d495
--- /dev/null
+++ b/web/src/pages/Profile/Cases/index.tsx
@@ -0,0 +1,68 @@
+import React, { useMemo } from "react";
+import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+
+import styled from "styled-components";
+import { responsiveSize } from "styles/responsiveSize";
+
+import { isUndefined } from "utils/index";
+import { decodeURIFilter, useRootPath } from "utils/uri";
+
+import { DisputeDetailsFragment, OrderDirection } from "src/graphql/graphql";
+import { useMyCasesQuery } from "queries/useCasesQuery";
+import { useUserQuery } from "queries/useUser";
+import CasesDisplay from "components/CasesDisplay";
+
+const StyledCasesDisplay = styled(CasesDisplay)`
+ margin-top: ${responsiveSize(24, 32)};
+
+ .title {
+ margin-bottom: ${responsiveSize(12, 24)};
+ }
+`;
+
+interface ICases {
+ searchParamAddress: `0x${string}`;
+}
+
+const Cases: React.FC = ({ searchParamAddress }) => {
+ const { page, order, filter } = useParams();
+ const [searchParams] = useSearchParams();
+ const location = useRootPath();
+ const navigate = useNavigate();
+
+ const casesPerPage = 3;
+ const pageNumber = parseInt(page ?? "1");
+ const disputeSkip = casesPerPage * (pageNumber - 1);
+ const decodedFilter = decodeURIFilter(filter ?? "all");
+ const { data: disputesData } = useMyCasesQuery(
+ searchParamAddress,
+ disputeSkip,
+ decodedFilter,
+ order === "asc" ? OrderDirection.Asc : OrderDirection.Desc
+ );
+
+ const { data: userData } = useUserQuery(searchParamAddress, decodedFilter);
+ const totalCases = userData?.user?.disputes.length;
+ const totalResolvedCases = parseInt(userData?.user?.totalResolvedDisputes);
+ const totalPages = useMemo(
+ () => (!isUndefined(totalCases) ? Math.ceil(totalCases / casesPerPage) : 1),
+ [totalCases, casesPerPage]
+ );
+
+ return (
+
+ navigate(`${location}/${newPage}/${order}/${filter}?${searchParams.toString()}`)
+ }
+ {...{ casesPerPage }}
+ />
+ );
+};
+
+export default Cases;
diff --git a/web/src/pages/Profile/JurorInfo/Coherence.tsx b/web/src/pages/Profile/JurorCard/BottomContent/Coherence.tsx
similarity index 94%
rename from web/src/pages/Profile/JurorInfo/Coherence.tsx
rename to web/src/pages/Profile/JurorCard/BottomContent/Coherence.tsx
index 712884d22..44e1f9786 100644
--- a/web/src/pages/Profile/JurorInfo/Coherence.tsx
+++ b/web/src/pages/Profile/JurorCard/BottomContent/Coherence.tsx
@@ -1,9 +1,11 @@
import React from "react";
import styled, { css } from "styled-components";
+import { landscapeStyle } from "styles/landscapeStyle";
+
import { CircularProgress } from "@kleros/ui-components-library";
-import { landscapeStyle } from "styles/landscapeStyle";
+import { ILevelCriteria } from "utils/userLevelCalculation";
import WithHelpTooltip from "components/WithHelpTooltip";
@@ -26,10 +28,7 @@ const tooltipMsg =
" the majority of jurors it's considered a Coherent Vote.";
interface ICoherence {
- userLevelData: {
- level: number;
- title: string;
- };
+ userLevelData: ILevelCriteria;
totalCoherentVotes: number;
totalResolvedVotes: number;
isMiniGuide: boolean;
diff --git a/web/src/pages/Profile/JurorInfo/JurorRewards.tsx b/web/src/pages/Profile/JurorCard/BottomContent/JurorRewards.tsx
similarity index 58%
rename from web/src/pages/Profile/JurorInfo/JurorRewards.tsx
rename to web/src/pages/Profile/JurorCard/BottomContent/JurorRewards.tsx
index 0a4471252..96adeab1f 100644
--- a/web/src/pages/Profile/JurorInfo/JurorRewards.tsx
+++ b/web/src/pages/Profile/JurorCard/BottomContent/JurorRewards.tsx
@@ -1,7 +1,7 @@
import React from "react";
-import styled from "styled-components";
+import styled, { css } from "styled-components";
-import { useAccount } from "wagmi";
+import { landscapeStyle } from "styles/landscapeStyle";
import { CoinIds } from "consts/coingecko";
import { useCoinPrice } from "hooks/useCoinPrice";
@@ -10,14 +10,28 @@ import { getFormattedRewards } from "utils/jurorRewardConfig";
import { useUserQuery } from "queries/useUser";
import WithHelpTooltip from "components/WithHelpTooltip";
-
-import TokenRewards from "./TokenRewards";
+import TokenRewards from "../TokenRewards";
const Container = styled.div`
display: flex;
flex-direction: column;
- align-items: flex-start;
+ align-items: center;
width: auto;
+ gap: 12px;
+
+ ${landscapeStyle(
+ () => css`
+ align-items: flex-start;
+ gap: 24px;
+ `
+ )}
+`;
+
+const TokenRewardsContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: 16px;
`;
const tooltipMsg =
@@ -27,11 +41,11 @@ const tooltipMsg =
"arbitration fees (ETH) + PNK redistribution between jurors.";
interface IJurorRewards {
- addressToQuery: `0x${string}`;
+ searchParamAddress: `0x${string}`;
}
-const JurorRewards: React.FC = ({ addressToQuery }) => {
- const { data } = useUserQuery(addressToQuery);
+const JurorRewards: React.FC = ({ searchParamAddress }) => {
+ const { data } = useUserQuery(searchParamAddress);
const coinIds = [CoinIds.PNK, CoinIds.ETH];
const { prices: pricesData } = useCoinPrice(coinIds);
@@ -42,9 +56,11 @@ const JurorRewards: React.FC = ({ addressToQuery }) => {
Juror Rewards
- {formattedRewards.map(({ token, amount, value }) => (
-
- ))}
+
+ {formattedRewards.map(({ token, amount, value }) => (
+
+ ))}
+
);
};
diff --git a/web/src/pages/Profile/JurorInfo/PixelArt.tsx b/web/src/pages/Profile/JurorCard/BottomContent/PixelArt.tsx
similarity index 93%
rename from web/src/pages/Profile/JurorInfo/PixelArt.tsx
rename to web/src/pages/Profile/JurorCard/BottomContent/PixelArt.tsx
index 48e15dd93..59a909a82 100644
--- a/web/src/pages/Profile/JurorInfo/PixelArt.tsx
+++ b/web/src/pages/Profile/JurorCard/BottomContent/PixelArt.tsx
@@ -9,6 +9,11 @@ import platoImage from "assets/pngs/dashboard/plato.png";
import pythagorasImage from "assets/pngs/dashboard/pythagoras.png";
import socratesImage from "assets/pngs/dashboard/socrates.png";
+const Container = styled.div`
+ display: flex;
+ justify-content: center;
+`;
+
interface IStyledImage {
show: boolean;
width: number | string;
@@ -42,7 +47,7 @@ interface IPixelArt {
const PixelArt: React.FC = ({ level, width, height }) => {
const [imageLoaded, setImageLoaded] = useState(false);
return (
-
+
{!imageLoaded && }
= ({ level, width, height }) => {
width={width}
height={height}
/>
-
+
);
};
diff --git a/web/src/pages/Profile/JurorCard/BottomContent/index.tsx b/web/src/pages/Profile/JurorCard/BottomContent/index.tsx
new file mode 100644
index 000000000..34749232b
--- /dev/null
+++ b/web/src/pages/Profile/JurorCard/BottomContent/index.tsx
@@ -0,0 +1,69 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { landscapeStyle } from "styles/landscapeStyle";
+
+import { ILevelCriteria } from "utils/userLevelCalculation";
+
+import PixelArt from "./PixelArt";
+import Coherence from "./Coherence";
+import JurorRewards from "./JurorRewards";
+import StakingRewards from "../StakingRewards";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
+
+ gap: 32px;
+ width: 100%;
+ height: auto;
+
+ ${landscapeStyle(
+ () => css`
+ flex-direction: row;
+ align-items: flex-start;
+ `
+ )}
+`;
+
+const LeftContent = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 48px;
+ flex-direction: column;
+
+ ${landscapeStyle(
+ () => css`
+ flex-direction: row;
+ `
+ )}
+`;
+
+interface IBottomContent {
+ userLevelData: ILevelCriteria;
+ totalCoherentVotes: number;
+ totalResolvedVotes: number;
+ searchParamAddress: `0x${string}`;
+}
+
+const BottomContent: React.FC = ({
+ userLevelData,
+ totalCoherentVotes,
+ totalResolvedVotes,
+ searchParamAddress,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+export default BottomContent;
diff --git a/web/src/pages/Profile/JurorInfo/Header.tsx b/web/src/pages/Profile/JurorCard/Header.tsx
similarity index 60%
rename from web/src/pages/Profile/JurorInfo/Header.tsx
rename to web/src/pages/Profile/JurorCard/Header.tsx
index 59051816b..fed750f01 100644
--- a/web/src/pages/Profile/JurorInfo/Header.tsx
+++ b/web/src/pages/Profile/JurorCard/Header.tsx
@@ -1,17 +1,12 @@
-import React, { useMemo } from "react";
+import React 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";
@@ -46,21 +41,9 @@ const StyledXIcon = styled(XIcon)`
fill: ${({ theme }) => theme.primaryBlue};
`;
-const StyledJurorExternalLink = styled(ExternalLink)`
- font-size: ${responsiveSize(18, 22)};
- margin-left: ${responsiveSize(4, 8)};
- font-weight: 600;
-`;
-
-const StyledShareExternalLink = styled(ExternalLink)`
+const StyledLink = styled(ExternalLink)`
display: flex;
- gap: 6px;
-`;
-
-const StyledShareLabel = styled.label`
- margin-top: 1px;
- color: ${({ theme }) => theme.primaryBlue};
- cursor: pointer;
+ gap: 8px;
`;
interface IHeader {
@@ -68,7 +51,7 @@ interface IHeader {
levelNumber: number;
totalCoherentVotes: number;
totalResolvedVotes: number;
- addressToQuery: `0x${string}`;
+ searchParamAddress: `0x${string}`;
}
const Header: React.FC = ({
@@ -76,31 +59,17 @@ const Header: React.FC = ({
levelNumber,
totalCoherentVotes,
totalResolvedVotes,
- addressToQuery,
+ searchParamAddress,
}) => {
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 (
-
- Juror Profile -
-
-
- {shortenAddress(addressToQuery)}
-
-
-
+ Juror Profile
= ({
MiniGuideComponent={JurorLevels}
/>
{totalResolvedVotes > 0 && !searchParamAddress ? (
-
-
- Share your juror score
-
+
+ Share your juror score
+
) : null}
diff --git a/web/src/pages/Profile/JurorInfo/StakingRewards.tsx b/web/src/pages/Profile/JurorCard/StakingRewards.tsx
similarity index 61%
rename from web/src/pages/Profile/JurorInfo/StakingRewards.tsx
rename to web/src/pages/Profile/JurorCard/StakingRewards.tsx
index 1e699c7de..c8510a483 100644
--- a/web/src/pages/Profile/JurorInfo/StakingRewards.tsx
+++ b/web/src/pages/Profile/JurorCard/StakingRewards.tsx
@@ -6,12 +6,13 @@ import { Box as _Box, Button } from "@kleros/ui-components-library";
import { EnsureChain } from "components/EnsureChain";
import WithHelpTooltip from "components/WithHelpTooltip";
-import TokenRewards from "./TokenRewards";
+// import TokenRewards from "./TokenRewards";
const Container = styled.div`
display: flex;
flex-direction: column;
- align-items: flex-start;
+ align-items: center;
+ gap: 4px;
`;
const Box = styled(_Box)`
@@ -46,20 +47,28 @@ const ClaimPNK: React.FC = () => {
const tooltipMsg =
"Staking Rewards are the rewards won by staking your PNK on a court during " +
- "the Kleros' Jurors incentive program.";
+ "the Kleros' Jurors incentive program. This will start as soon as the " +
+ "corresponding KIP (Kleros Improvement Proposal) goes into effect.";
-const Coherence: React.FC = () => {
+const StakingRewards: React.FC = () => {
return (
+ //
+ //
+ //
+ // Staking Rewards: APY 6%
+ //
+ // Coming soon
+ //
+ //
+ //
+ //
-
- Staking Rewards: APY 6%
-
+ Staking Rewards
-
-
+ Coming soon
);
};
-export default Coherence;
+export default StakingRewards;
diff --git a/web/src/pages/Profile/JurorInfo/TokenRewards.tsx b/web/src/pages/Profile/JurorCard/TokenRewards.tsx
similarity index 100%
rename from web/src/pages/Profile/JurorInfo/TokenRewards.tsx
rename to web/src/pages/Profile/JurorCard/TokenRewards.tsx
diff --git a/web/src/pages/Profile/JurorCard/TopContent/index.tsx b/web/src/pages/Profile/JurorCard/TopContent/index.tsx
new file mode 100644
index 000000000..d917ff537
--- /dev/null
+++ b/web/src/pages/Profile/JurorCard/TopContent/index.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import styled from "styled-components";
+
+import JurorLink from "components/JurorLink";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: row
+ align-items: center;
+ gap: 16px 24px;
+ flex-wrap: wrap;
+`;
+
+const StyledLabel = styled.label`
+ font-size: 14px;
+`;
+
+interface ITopContent {
+ address: `0x${string}`;
+ totalResolvedDisputes: number;
+}
+
+const TopContent: React.FC = ({ address, totalResolvedDisputes }) => {
+ return (
+
+
+ {totalResolvedDisputes > 0 ? (
+
+ Juror in {totalResolvedDisputes} {totalResolvedDisputes === 1 ? "case" : "cases"}
+
+ ) : null}
+
+ );
+};
+export default TopContent;
diff --git a/web/src/pages/Profile/JurorCard/index.tsx b/web/src/pages/Profile/JurorCard/index.tsx
new file mode 100644
index 000000000..b2f3b6656
--- /dev/null
+++ b/web/src/pages/Profile/JurorCard/index.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import styled from "styled-components";
+
+import { Card as _Card } from "@kleros/ui-components-library";
+
+import { getUserLevelData } from "utils/userLevelCalculation";
+
+import { useUserQuery } from "queries/useUser";
+
+import { Divider } from "components/Divider";
+
+import BottomContent from "./BottomContent";
+import Header from "./Header";
+import TopContent from "./TopContent";
+
+const Container = styled.div``;
+
+const Card = styled(_Card)`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ gap: 24px;
+ width: 100%;
+ height: auto;
+ padding: 24px;
+`;
+
+interface IJurorCard {
+ searchParamAddress: `0x${string}`;
+}
+
+const JurorCard: React.FC = ({ searchParamAddress }) => {
+ const { data } = useUserQuery(searchParamAddress);
+ 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 coherenceScore = data?.user ? parseInt(data?.user?.coherenceScore) : 0;
+ const userLevelData = getUserLevelData(coherenceScore);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default JurorCard;
diff --git a/web/src/pages/Profile/JurorInfo/index.tsx b/web/src/pages/Profile/JurorInfo/index.tsx
deleted file mode 100644
index 87071c220..000000000
--- a/web/src/pages/Profile/JurorInfo/index.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from "react";
-import styled, { css } from "styled-components";
-
-import { Card as _Card } from "@kleros/ui-components-library";
-
-import { getUserLevelData } from "utils/userLevelCalculation";
-
-import { useUserQuery } from "queries/useUser";
-
-import { landscapeStyle } from "styles/landscapeStyle";
-import { responsiveSize } from "styles/responsiveSize";
-
-import Coherence from "./Coherence";
-import Header from "./Header";
-import JurorRewards from "./JurorRewards";
-import PixelArt from "./PixelArt";
-
-const Container = styled.div``;
-
-const Card = styled(_Card)`
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
-
- gap: 40px;
- width: 100%;
- height: auto;
- padding: 24px 0;
-
- ${landscapeStyle(
- () => css`
- flex-direction: row;
- gap: ${responsiveSize(24, 64)};
- height: 236px;
- `
- )}
-`;
-
-interface IJurorInfo {
- addressToQuery: `0x${string}`;
-}
-
-const JurorInfo: React.FC = ({ addressToQuery }) => {
- const { data } = useUserQuery(addressToQuery);
- const totalCoherentVotes = data?.user ? parseInt(data?.user?.totalCoherentVotes) : 0;
- const totalResolvedVotes = data?.user ? parseInt(data?.user?.totalResolvedVotes) : 0;
- const coherenceScore = data?.user ? parseInt(data?.user?.coherenceScore) : 0;
- const userLevelData = getUserLevelData(coherenceScore);
-
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-export default JurorInfo;
diff --git a/web/src/pages/Profile/Stakes/CourtCard/CourtName.tsx b/web/src/pages/Profile/Stakes/CourtCard/CourtName.tsx
index 6b53b480d..443503dc9 100644
--- a/web/src/pages/Profile/Stakes/CourtCard/CourtName.tsx
+++ b/web/src/pages/Profile/Stakes/CourtCard/CourtName.tsx
@@ -3,17 +3,14 @@ import styled, { css } from "styled-components";
import { landscapeStyle } from "styles/landscapeStyle";
-import ArrowIcon from "svgs/icons/arrow.svg";
-
-import { StyledArrowLink } from "components/StyledArrowLink";
-
const Container = styled.div`
display: flex;
width: 100%;
flex-direction: row;
- gap: 16px;
+ gap: 8px 16px;
align-items: center;
justify-content: space-between;
+ flex-wrap: wrap;
small {
height: 100%;
@@ -28,15 +25,6 @@ const Container = styled.div`
)}
`;
-const ReStyledArrowLink = styled(StyledArrowLink)`
- font-size: 14px;
-
- > svg {
- height: 15px;
- width: 15px;
- }
-`;
-
interface ICourtName {
name: string;
id: string;
@@ -46,9 +34,6 @@ const CourtName: React.FC = ({ name, id }) => {
return (
{name}
-
- Open Court
-
);
};
diff --git a/web/src/pages/Profile/Stakes/CourtCard/Stake.tsx b/web/src/pages/Profile/Stakes/CourtCard/Stake.tsx
index c1617b621..168d30ff5 100644
--- a/web/src/pages/Profile/Stakes/CourtCard/Stake.tsx
+++ b/web/src/pages/Profile/Stakes/CourtCard/Stake.tsx
@@ -1,30 +1,10 @@
import React from "react";
-import styled, { css } from "styled-components";
+import styled from "styled-components";
import { formatUnits } from "viem";
-import { landscapeStyle } from "styles/landscapeStyle";
-
import NumberDisplay from "components/NumberDisplay";
-import PnkIcon from "svgs/icons/pnk.svg";
-
-const Container = styled.div`
- display: flex;
- flex-direction: row;
- gap: 8px;
- width: 100%;
- justify-content: flex-start;
- align-items: center;
-
- ${landscapeStyle(
- () => css`
- width: auto;
- gap: 12px;
- `
- )}
-`;
-
const StyledLabel = styled.label`
display: flex;
font-weight: 600;
@@ -34,13 +14,6 @@ const StyledLabel = styled.label`
gap: 4px;
`;
-const StyledPnkIcon = styled(PnkIcon)`
- display: inline-block;
- width: 16px;
- height: 16px;
- fill: ${({ theme }) => theme.secondaryPurple};
-`;
-
interface IStake {
stake: string;
}
@@ -49,12 +22,9 @@ const Stake: React.FC = ({ stake }) => {
const formattedStake = formatUnits(stake, 18);
return (
-
-
-
-
-
-
+
+
+
);
};
export default Stake;
diff --git a/web/src/pages/Profile/Stakes/CourtCard/index.tsx b/web/src/pages/Profile/Stakes/CourtCard/index.tsx
index 360f97ca6..670a8ec92 100644
--- a/web/src/pages/Profile/Stakes/CourtCard/index.tsx
+++ b/web/src/pages/Profile/Stakes/CourtCard/index.tsx
@@ -1,14 +1,21 @@
import React from "react";
import styled, { css } from "styled-components";
+import { landscapeStyle } from "styles/landscapeStyle";
+
import { Card as _Card } from "@kleros/ui-components-library";
-import { landscapeStyle } from "styles/landscapeStyle";
+import ArrowIcon from "svgs/icons/arrow.svg";
+import NewTabIcon from "svgs/icons/new-tab.svg";
+import { formatDate } from "utils/date";
+import { getTxnExplorerLink } from "utils/index";
+
+import { StyledArrowLink } from "components/StyledArrowLink";
import CourtName from "./CourtName";
import Stake from "./Stake";
-const Container = styled(_Card)`
+const Container = styled(_Card)<{ isCurrentStakeCard?: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
@@ -16,7 +23,8 @@ const Container = styled(_Card)`
height: auto;
width: 100%;
padding: 20px 16px 24px;
- border-left: 5px solid ${({ theme }) => theme.secondaryPurple};
+ border-left: 5px solid
+ ${({ theme, isCurrentStakeCard }) => (isCurrentStakeCard ? theme.secondaryPurple : theme.secondaryText)};
flex-wrap: wrap;
gap: 16px;
@@ -28,22 +36,76 @@ const Container = styled(_Card)`
${landscapeStyle(
() => css`
- padding: 21.5px 32px;
+ padding: 21.5px 28px;
`
)}
`;
+const LeftContent = styled.div`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 16px 24px;
+`;
+
+const StakeAndLinkAndDateContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 16px;
+`;
+
+const StakeAndLink = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+`;
+
+const ReStyledArrowLink = styled(StyledArrowLink)`
+ font-size: 14px;
+
+ > svg {
+ height: 15px;
+ width: 15px;
+ }
+`;
+
interface ICourtCard {
name: string;
stake: string;
id: string;
+ timestamp?: number;
+ transactionHash?: string;
+ isCurrentStakeCard?: boolean;
}
-const CourtCard: React.FC = ({ name, stake, id }) => {
+const CourtCard: React.FC = ({
+ name,
+ stake,
+ id,
+ timestamp,
+ transactionHash,
+ isCurrentStakeCard = true,
+}) => {
return (
-
-
-
+
+
+
+
+
+
+ {transactionHash ? (
+
+
+
+ ) : null}
+
+ {timestamp ? {formatDate(timestamp)} : null}
+
+
+
+ Open Court
+
);
};
diff --git a/web/src/pages/Profile/Stakes/CurrentStakes/Header.tsx b/web/src/pages/Profile/Stakes/CurrentStakes/Header.tsx
new file mode 100644
index 000000000..813342fcd
--- /dev/null
+++ b/web/src/pages/Profile/Stakes/CurrentStakes/Header.tsx
@@ -0,0 +1,103 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { formatUnits } from "viem";
+
+import LockerIcon from "svgs/icons/locker.svg";
+import PnkIcon from "svgs/icons/pnk.svg";
+
+import { isUndefined } from "utils/index";
+
+import { landscapeStyle } from "styles/landscapeStyle";
+import { responsiveSize } from "styles/responsiveSize";
+
+import NumberDisplay from "components/NumberDisplay";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ width: 100%;
+ gap: 4px 16px;
+ align-items: center;
+ margin-bottom: 20px;
+
+ ${landscapeStyle(
+ () => css`
+ justify-content: space-between;
+ `
+ )}
+`;
+
+const StakedPnk = styled.div`
+ display: flex;
+ gap: 8px;
+ align-items: center;
+`;
+
+const LockedPnk = styled.div`
+ display: flex;
+ gap: 8px;
+ align-items: center;
+`;
+
+const StyledTitle = styled.h1`
+ margin-bottom: 0;
+ font-size: ${responsiveSize(20, 24)};
+`;
+
+const TotalStakeAndLockedPnk = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 12px 24px;
+ flex-wrap: wrap;
+`;
+
+const StyledPnkIcon = styled(PnkIcon)`
+ fill: ${({ theme }) => theme.secondaryPurple};
+ width: 16px;
+`;
+
+const StyledLockerIcon = styled(LockerIcon)`
+ fill: ${({ theme }) => theme.secondaryPurple};
+ width: 14px;
+`;
+
+interface IHeader {
+ totalAvailableStake: string;
+ lockedStake: string;
+}
+
+const Header: React.FC = ({ totalAvailableStake, lockedStake }) => {
+ const formattedTotalAvailableStake = !isUndefined(totalAvailableStake)
+ ? formatUnits(BigInt(totalAvailableStake), 18)
+ : "0";
+ const formattedLockedStake = !isUndefined(lockedStake) ? formatUnits(BigInt(lockedStake), 18) : "0";
+
+ return (
+
+ Current Stakes
+
+ {!isUndefined(totalAvailableStake) ? (
+
+
+ Total Available Stake:
+
+
+
+
+ ) : null}
+ {!isUndefined(lockedStake) ? (
+
+
+ Locked Stake:
+
+
+
+
+ ) : null}
+
+
+ );
+};
+export default Header;
diff --git a/web/src/pages/Profile/Stakes/CurrentStakes/index.tsx b/web/src/pages/Profile/Stakes/CurrentStakes/index.tsx
new file mode 100644
index 000000000..3d93e4ee3
--- /dev/null
+++ b/web/src/pages/Profile/Stakes/CurrentStakes/index.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import styled from "styled-components";
+
+import Skeleton from "react-loading-skeleton";
+
+import { JurorStakeDetailsQuery } from "src/graphql/graphql";
+
+import { responsiveSize } from "styles/responsiveSize";
+
+import CourtCard from "../CourtCard";
+import { CourtCardsContainer } from "../index";
+
+import Header from "./Header";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+`;
+
+const NoCurrentStakesLabel = styled.label`
+ font-size: ${responsiveSize(14, 16)};
+`;
+
+interface ICurrentStakes {
+ totalAvailableStake: string;
+ lockedStake: string;
+ currentStakeData: JurorStakeDetailsQuery | undefined;
+ isCurrentStakeLoading: boolean;
+}
+
+const CurrentStakes: React.FC = ({
+ totalAvailableStake,
+ lockedStake,
+ currentStakeData,
+ isCurrentStakeLoading,
+}) => {
+ const stakedCourts = currentStakeData?.jurorTokensPerCourts?.filter(({ staked }) => staked > 0);
+ const isStaked = stakedCourts && stakedCourts.length > 0;
+
+ return (
+
+
+ {!isStaked && !isCurrentStakeLoading ? (
+ No stakes found
+ ) : isCurrentStakeLoading ? (
+
+ ) : null}
+ {isStaked && !isCurrentStakeLoading ? (
+
+ {currentStakeData?.jurorTokensPerCourts
+ ?.filter(({ staked }) => staked > 0)
+ .map(({ court: { id, name }, staked }) => (
+
+ ))}
+
+ ) : null}
+
+ );
+};
+export default CurrentStakes;
diff --git a/web/src/pages/Profile/Stakes/StakingHistory.tsx b/web/src/pages/Profile/Stakes/StakingHistory.tsx
new file mode 100644
index 000000000..20f23e5dc
--- /dev/null
+++ b/web/src/pages/Profile/Stakes/StakingHistory.tsx
@@ -0,0 +1,103 @@
+import React, { useMemo } from "react";
+import styled from "styled-components";
+
+import Skeleton from "react-loading-skeleton";
+import { useParams, useNavigate } from "react-router-dom";
+
+import { StandardPagination } from "@kleros/ui-components-library";
+
+import { useStakingEventsByCourt } from "hooks/useStakingEventsByCourt";
+import { findCourtNameById } from "utils/findCourtNameById";
+
+import { useCourtTree } from "queries/useCourtTree";
+
+import { responsiveSize } from "styles/responsiveSize";
+
+import CourtCard from "./CourtCard";
+
+import { CourtCardsContainer } from "./index";
+
+const Container = styled.div``;
+
+const StyledPagination = styled(StandardPagination)`
+ margin-top: 24px;
+ margin-left: auto;
+ margin-right: auto;
+`;
+
+const StyledTitle = styled.h1`
+ font-size: ${responsiveSize(20, 24)};
+ margin-bottom: 20px;
+`;
+
+const NoHistoryLabel = styled.label`
+ font-size: ${responsiveSize(14, 16)};
+`;
+
+interface IStakingHistory {
+ searchParamAddress: `0x${string}`;
+}
+
+const StakingHistory: React.FC = ({ searchParamAddress }) => {
+ const { page } = useParams();
+ const navigate = useNavigate();
+ const eventsPerPage = 10;
+ const currentPage = parseInt(page ?? "1");
+ const skip = (currentPage - 1) * eventsPerPage;
+
+ const { data: stakingHistoryData, isFetching: isLoadingStakingHistory } = useStakingEventsByCourt(
+ [],
+ skip,
+ eventsPerPage,
+ searchParamAddress
+ );
+
+ const { data: courtTreeData, isLoading: isLoadingCourtTree } = useCourtTree();
+ const totalNumberStakingEvents = stakingHistoryData?.userStakingEvents?.count ?? 0;
+ const totalPages = useMemo(() => Math.ceil(totalNumberStakingEvents / eventsPerPage), [totalNumberStakingEvents]);
+
+ // Sort by timestamp descending (latest first) - API doesn't support orderBy parameter
+ const sortedStakingEvents = useMemo(() => {
+ const events = stakingHistoryData?.userStakingEvents?.items ?? [];
+ return [...events].sort((a, b) => parseInt(b.item.blockTimestamp) - parseInt(a.item.blockTimestamp));
+ }, [stakingHistoryData?.userStakingEvents?.items]);
+
+ const handlePageChange = (newPage: number) => {
+ navigate(`/profile/stakes/${newPage}?address=${searchParamAddress}`);
+ };
+
+ return (
+
+ Staking History
+
+ {!isLoadingStakingHistory && totalNumberStakingEvents === 0 ? (
+ No history found
+ ) : isLoadingStakingHistory || isLoadingCourtTree ? (
+ Array.from({ length: 10 }).map((_, index) => )
+ ) : (
+ <>
+ {sortedStakingEvents.map(({ item }) => {
+ const courtName = findCourtNameById(courtTreeData, item.args._courtID);
+ return (
+
+ );
+ })}
+ {totalPages > 1 && (
+
+ )}
+ >
+ )}
+
+
+ );
+};
+
+export default StakingHistory;
diff --git a/web/src/pages/Profile/Stakes/index.tsx b/web/src/pages/Profile/Stakes/index.tsx
index 8df01bb0b..08f1575ae 100644
--- a/web/src/pages/Profile/Stakes/index.tsx
+++ b/web/src/pages/Profile/Stakes/index.tsx
@@ -1,9 +1,6 @@
import React from "react";
import styled, { css } from "styled-components";
-import Skeleton from "react-loading-skeleton";
-import { useSearchParams } from "react-router-dom";
-
import { useReadSortitionModuleGetJurorBalance } from "hooks/contracts/generated";
import { useJurorStakeDetailsQuery } from "queries/useJurorStakeDetailsQuery";
@@ -11,65 +8,46 @@ import { useJurorStakeDetailsQuery } from "queries/useJurorStakeDetailsQuery";
import { landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";
-import CourtCard from "./CourtCard";
-import Header from "./Header";
+import CurrentStakes from "./CurrentStakes";
+import StakingHistory from "./StakingHistory";
const Container = styled.div`
- margin-top: ${responsiveSize(24, 48)};
+ display: flex;
+ flex-direction: column;
+ margin-top: ${responsiveSize(24, 32)};
+ gap: 32px;
`;
-const CourtCardsContainer = styled.div`
+export const CourtCardsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
z-index: 0;
+ width: 100%;
${landscapeStyle(
() => css`
- gap: 16px;
+ gap: 8px;
`
)}
`;
-const StyledLabel = styled.label`
- font-size: ${responsiveSize(14, 16)};
-`;
-
interface IStakes {
- addressToQuery: `0x${string}`;
+ searchParamAddress: `0x${string}`;
}
-const Stakes: React.FC = ({ addressToQuery }) => {
- const { data: stakeData, isLoading } = useJurorStakeDetailsQuery(addressToQuery);
+const Stakes: React.FC = ({ searchParamAddress }) => {
+ const { data: currentStakeData, isLoading: isCurrentStakeLoading } = useJurorStakeDetailsQuery(searchParamAddress);
const { data: jurorBalance } = useReadSortitionModuleGetJurorBalance({
- args: [addressToQuery, BigInt(1)],
+ args: [searchParamAddress, 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 availableStake = jurorBalance?.[0];
+ const totalAvailableStake = jurorBalance?.[0];
const lockedStake = jurorBalance?.[1];
- const effectiveStake = stakeData?.jurorTokensPerCourts?.[0]?.effectiveStake
- ? BigInt(stakeData.jurorTokensPerCourts[0].effectiveStake)
- : undefined;
return (
-
- {isLoading ? : null}
- {!isStaked && !isLoading ? (
- {searchParamAddress ? "They" : "You"} are not staked in any court
- ) : null}
- {isStaked && !isLoading ? (
-
- {stakeData?.jurorTokensPerCourts
- ?.filter(({ staked }) => staked > 0)
- .map(({ court: { id, name }, staked }) => (
-
- ))}
-
- ) : null}
+
+
);
};
diff --git a/web/src/pages/Profile/Votes/StatsAndFilters/Filters.tsx b/web/src/pages/Profile/Votes/StatsAndFilters/Filters.tsx
new file mode 100644
index 000000000..f02abb785
--- /dev/null
+++ b/web/src/pages/Profile/Votes/StatsAndFilters/Filters.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import styled, { useTheme } from "styled-components";
+
+import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+
+import { DropdownSelect } from "@kleros/ui-components-library";
+
+import { decodeURIFilter, encodeURIFilter, useRootPath } from "utils/uri";
+
+const Container = styled.div`
+ display: flex;
+ justify-content: end;
+ gap: 12px;
+ width: fit-content;
+`;
+
+const Filters: React.FC = () => {
+ const theme = useTheme();
+ const { order, filter } = useParams();
+ 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}?${searchParams.toString()}`);
+ };
+
+ const handleOrderChange = (value: string | number) => {
+ const encodedFilter = encodeURIFilter({ ruled, period, ...filterObject });
+ navigate(`${location}/1/${value}/${encodedFilter}?${searchParams.toString()}`);
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default Filters;
diff --git a/web/src/pages/Profile/Votes/StatsAndFilters/Stats.tsx b/web/src/pages/Profile/Votes/StatsAndFilters/Stats.tsx
new file mode 100644
index 000000000..e64e292a5
--- /dev/null
+++ b/web/src/pages/Profile/Votes/StatsAndFilters/Stats.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+import styled from "styled-components";
+
+const FieldWrapper = styled.div`
+ display: inline-flex;
+ gap: 8px;
+`;
+
+const SeparatorLabel = styled.label`
+ margin: 0 8px;
+ color: ${({ theme }) => theme.primaryText};
+`;
+
+const StyledLabel = styled.label`
+ color: ${({ theme }) => theme.primaryText};
+`;
+
+const Field: React.FC<{ label: string; value: string }> = ({ label, value }) => (
+
+ {label}
+ {value}
+
+);
+
+const Separator: React.FC = () => | ;
+
+export interface IStats {
+ totalVotes: number;
+ votesPending: number;
+ resolvedVotes: number;
+}
+
+const Stats: React.FC = ({ totalVotes, votesPending, resolvedVotes }) => {
+ const casesInProgress = (totalVotes - resolvedVotes).toString();
+
+ const fields = [
+ { label: "Total", value: totalVotes.toString() },
+ { label: "Vote Pending", value: votesPending },
+ { label: "Case In Progress", value: casesInProgress },
+ { label: "Resolved", value: resolvedVotes.toString() },
+ ];
+
+ return (
+
+ {fields.map(({ label, value }, i) => (
+
+
+ {i + 1 < fields.length ? : null}
+
+ ))}
+
+ );
+};
+
+export default Stats;
diff --git a/web/src/pages/Profile/Votes/StatsAndFilters/index.tsx b/web/src/pages/Profile/Votes/StatsAndFilters/index.tsx
new file mode 100644
index 000000000..970e4c05e
--- /dev/null
+++ b/web/src/pages/Profile/Votes/StatsAndFilters/index.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import styled from "styled-components";
+
+import { responsiveSize } from "styles/responsiveSize";
+
+import Filters from "./Filters";
+import Stats, { IStats } from "./Stats";
+
+const Container = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: ${responsiveSize(4, 12)};
+ justify-content: space-between;
+`;
+
+const StatsAndFilters: React.FC = ({ totalVotes, votesPending, resolvedVotes }) => (
+
+
+
+
+);
+
+export default StatsAndFilters;
diff --git a/web/src/pages/Profile/Votes/VoteCard/CaseNumber.tsx b/web/src/pages/Profile/Votes/VoteCard/CaseNumber.tsx
new file mode 100644
index 000000000..c7f37e32e
--- /dev/null
+++ b/web/src/pages/Profile/Votes/VoteCard/CaseNumber.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { landscapeStyle } from "styles/landscapeStyle";
+
+import { InternalLink } from "components/InternalLink";
+
+const Container = styled.div`
+ display: flex;
+ width: 100%;
+ flex-direction: row;
+ gap: 8px 16px;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+
+ small {
+ height: 100%;
+ font-weight: 600;
+ }
+
+ ${landscapeStyle(
+ () => css`
+ justify-content: flex-start;
+ width: auto;
+ `
+ )}
+`;
+
+const StyledInternalLink = styled(InternalLink)`
+ font-weight: 600;
+`;
+
+interface ICaseNumber {
+ id: string;
+}
+
+const CaseNumber: React.FC = ({ id }) => {
+ return (
+
+ Case {id}
+
+ );
+};
+export default CaseNumber;
diff --git a/web/src/pages/Profile/Votes/VoteCard/CaseStatus.tsx b/web/src/pages/Profile/Votes/VoteCard/CaseStatus.tsx
new file mode 100644
index 000000000..c1290cddc
--- /dev/null
+++ b/web/src/pages/Profile/Votes/VoteCard/CaseStatus.tsx
@@ -0,0 +1,62 @@
+import React, { useMemo } from "react";
+import styled, { css, useTheme } from "styled-components";
+
+import { Periods } from "consts/periods";
+
+import { getPeriodColors } from "components/DisputeView/PeriodBanner";
+
+interface ICaseStatus {}
+
+const StyledLabel = styled.label<{ frontColor: string; withDot?: boolean }>`
+ display: flex;
+ align-items: center;
+ width: auto;
+ color: ${({ frontColor }) => frontColor};
+ ${({ withDot, frontColor }) =>
+ withDot
+ ? css`
+ ::before {
+ content: "";
+ display: inline-block;
+ height: 8px;
+ width: 8px;
+ border-radius: 50%;
+ margin-right: 8px;
+ background-color: ${frontColor};
+ flex-shrink: 0;
+ }
+ `
+ : null}
+`;
+
+const getPeriodLabel = (period: Periods): string => {
+ switch (period) {
+ case Periods.evidence:
+ return "In Progress";
+ case Periods.commit:
+ return "In Progress";
+ case Periods.vote:
+ return "Voting";
+ case Periods.appeal:
+ return "Crowdfunding Appeal";
+ case Periods.execution:
+ return "Closed";
+ default:
+ return "In Progress";
+ }
+};
+
+const CaseStatus: React.FC = ({}) => {
+ const theme = useTheme();
+ const [frontColor, backgroundColor] = useMemo(
+ () => getPeriodColors(Periods.evidence, theme),
+ [theme, Periods.evidence]
+ );
+
+ return (
+
+ {getPeriodLabel(Periods.evidence)}
+
+ );
+};
+export default CaseStatus;
diff --git a/web/src/pages/Profile/Votes/VoteCard/CourtName.tsx b/web/src/pages/Profile/Votes/VoteCard/CourtName.tsx
new file mode 100644
index 000000000..74715c136
--- /dev/null
+++ b/web/src/pages/Profile/Votes/VoteCard/CourtName.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { landscapeStyle } from "styles/landscapeStyle";
+
+const Container = styled.div`
+ display: flex;
+ width: 100%;
+ flex-direction: row;
+ gap: 8px 16px;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+
+ small {
+ height: 100%;
+ font-weight: 400;
+ }
+
+ ${landscapeStyle(
+ () => css`
+ justify-content: flex-start;
+ width: auto;
+ `
+ )}
+`;
+
+interface ICourtName {
+ name: string;
+}
+
+const CourtName: React.FC = ({ name }) => {
+ return (
+
+ {name}
+
+ );
+};
+export default CourtName;
diff --git a/web/src/pages/Profile/Votes/VoteCard/Round.tsx b/web/src/pages/Profile/Votes/VoteCard/Round.tsx
new file mode 100644
index 000000000..7b0e4f40b
--- /dev/null
+++ b/web/src/pages/Profile/Votes/VoteCard/Round.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import styled from "styled-components";
+
+import RoundIcon from "svgs/icons/round.svg";
+
+const Container = styled.div`
+ display: flex;
+ gap: 8px;
+
+ small {
+ font-weight: 400;
+ }
+`;
+
+interface IRound {
+ number: string;
+}
+
+const Round: React.FC = ({ number }) => {
+ return (
+
+
+ Round {number}
+
+ );
+};
+export default Round;
diff --git a/web/src/pages/Profile/Votes/VoteCard/Vote.tsx b/web/src/pages/Profile/Votes/VoteCard/Vote.tsx
new file mode 100644
index 000000000..60fa5c431
--- /dev/null
+++ b/web/src/pages/Profile/Votes/VoteCard/Vote.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import styled from "styled-components";
+
+import VotedIcon from "svgs/icons/voted-ballot.svg";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+
+ small {
+ font-weight: 400;
+ }
+`;
+
+const StyledVotedIcon = styled(VotedIcon)`
+ path {
+ fill: ${({ theme }) => theme.primaryBlue};
+ }
+`;
+
+const BlueSmall = styled.small`
+ color: ${({ theme }) => theme.primaryBlue};
+`;
+
+interface IVote {
+ choice: string;
+}
+
+const Vote: React.FC = ({ choice }) => {
+ return (
+
+
+ Vote:
+ {choice}
+
+ );
+};
+export default Vote;
diff --git a/web/src/pages/Profile/Votes/VoteCard/index.tsx b/web/src/pages/Profile/Votes/VoteCard/index.tsx
new file mode 100644
index 000000000..6761bee6c
--- /dev/null
+++ b/web/src/pages/Profile/Votes/VoteCard/index.tsx
@@ -0,0 +1,80 @@
+import React from "react";
+import styled, { css } from "styled-components";
+
+import { landscapeStyle } from "styles/landscapeStyle";
+
+import { Card as _Card } from "@kleros/ui-components-library";
+
+import ArrowIcon from "svgs/icons/arrow.svg";
+
+import { StyledArrowLink } from "components/StyledArrowLink";
+import CourtName from "./CourtName";
+import CaseNumber from "./CaseNumber";
+import Vote from "./Vote";
+import Round from "./Round";
+import CaseStatus from "./CaseStatus";
+
+const Container = styled(_Card)`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ height: auto;
+ width: 100%;
+ padding: 20px 16px 24px;
+ border-left: 5px solid ${({ theme }) => theme.secondaryPurple};
+ flex-wrap: wrap;
+ gap: 16px;
+
+ :hover {
+ cursor: auto;
+ }
+
+ ${({ theme }) => (theme.name === "light" ? `box-shadow: 0px 2px 3px 0px ${theme.stroke};` : "")}
+
+ ${landscapeStyle(
+ () => css`
+ padding: 21.5px 28px;
+ `
+ )}
+`;
+
+const LeftContent = styled.div`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 16px 24px;
+`;
+
+const ReStyledArrowLink = styled(StyledArrowLink)`
+ font-size: 14px;
+
+ > svg {
+ height: 15px;
+ width: 15px;
+ }
+`;
+
+interface IVoteCard {}
+
+const VoteCard: React.FC = ({}) => {
+ const courtName = "Technical Court";
+ const caseId = "10";
+
+ return (
+
+
+
+
+
+
+
+
+
+ View vote
+
+
+ );
+};
+
+export default VoteCard;
diff --git a/web/src/pages/Profile/Votes/index.tsx b/web/src/pages/Profile/Votes/index.tsx
new file mode 100644
index 000000000..890dbe4da
--- /dev/null
+++ b/web/src/pages/Profile/Votes/index.tsx
@@ -0,0 +1,69 @@
+import React from "react";
+import styled from "styled-components";
+
+import { responsiveSize } from "styles/responsiveSize";
+
+import { StandardPagination } from "@kleros/ui-components-library";
+import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+
+import { useRootPath } from "utils/uri";
+
+import StatsAndFilters from "./StatsAndFilters";
+import VoteCard from "./VoteCard";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ margin-top: ${responsiveSize(24, 32)};
+ gap: 20px;
+`;
+
+const StyledTitle = styled.h1`
+ margin-bottom: 0;
+ font-size: ${responsiveSize(20, 24)};
+`;
+
+const StyledPagination = styled(StandardPagination)`
+ margin-top: 24px;
+ margin-left: auto;
+ margin-right: auto;
+`;
+
+const VotesCardContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+`;
+
+interface IVotes {
+ searchParamAddress: `0x${string}`;
+}
+
+const Votes: React.FC = ({ searchParamAddress }) => {
+ const { page, order, filter } = useParams();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const votesPerPage = 5;
+ const location = useRootPath();
+ const totalPages = 20; //TODO, HARDCODED FOR NOW
+ const currentPage = parseInt(page ?? "1");
+
+ const handlePageChange = (newPage: number) => {
+ navigate(`${location}/${newPage}/${order}/${filter}?${searchParams.toString()}`);
+ };
+
+ return (
+
+ Votes
+
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default Votes;
diff --git a/web/src/pages/Profile/index.tsx b/web/src/pages/Profile/index.tsx
index b52327eda..3602f64f5 100644
--- a/web/src/pages/Profile/index.tsx
+++ b/web/src/pages/Profile/index.tsx
@@ -1,27 +1,26 @@
-import React, { useMemo } from "react";
+import React, { useEffect } from "react";
import styled, { css } from "styled-components";
-import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+import { Routes, Route, useNavigate, useSearchParams, useLocation, Navigate } from "react-router-dom";
import { useAccount } from "wagmi";
-import { isUndefined } from "utils/index";
-import { decodeURIFilter, useRootPath } from "utils/uri";
+import { Tabs as TabsComponent } from "@kleros/ui-components-library";
-import { DisputeDetailsFragment, useMyCasesQuery } from "queries/useCasesQuery";
-import { useUserQuery } from "queries/useUser";
-
-import { Dispute_Filter, OrderDirection, UserDetailsFragment } from "src/graphql/graphql";
+import DocIcon from "svgs/icons/doc.svg";
+import PnkIcon from "svgs/icons/pnk.svg";
+import VotedIcon from "svgs/icons/voted-ballot.svg";
import { MAX_WIDTH_LANDSCAPE, landscapeStyle } from "styles/landscapeStyle";
import { responsiveSize } from "styles/responsiveSize";
-import CasesDisplay from "components/CasesDisplay";
import ConnectWallet from "components/ConnectWallet";
import FavoriteCases from "components/FavoriteCases";
import ScrollTop from "components/ScrollTop";
-import JurorInfo from "./JurorInfo";
+import Cases from "./Cases";
+import JurorCard from "./JurorCard";
import Stakes from "./Stakes";
+import Votes from "./Votes";
const Container = styled.div`
width: 100%;
@@ -37,11 +36,16 @@ const Container = styled.div`
)}
`;
-const StyledCasesDisplay = styled(CasesDisplay)`
- margin-top: ${responsiveSize(24, 48)};
-
- .title {
- margin-bottom: ${responsiveSize(12, 24)};
+const StyledTabs = styled(TabsComponent)`
+ width: 100%;
+ margin-top: ${responsiveSize(16, 32)};
+ > * {
+ display: flex;
+ flex-wrap: wrap;
+ font-size: ${responsiveSize(14, 16)};
+ > svg {
+ margin-right: 8px !important;
+ }
}
`;
@@ -53,87 +57,68 @@ const ConnectWalletContainer = styled.div`
color: ${({ theme }) => theme.primaryText};
`;
-const calculateStats = (user: UserDetailsFragment, filter: Dispute_Filter) => {
- const toInt = (v) => Number(v) || 0;
- let totalCases, ruledCases;
-
- if (!user) {
- totalCases = 0;
- ruledCases = 0;
- } else if (filter?.period === "appeal") {
- totalCases = toInt(user.totalAppealingDisputes);
- ruledCases = 0;
- } else if (filter?.ruled === true) {
- totalCases = toInt(user.totalResolvedDisputes);
- ruledCases = totalCases;
- } else if (filter?.ruled === false) {
- totalCases = toInt(user.disputes?.length);
- ruledCases = 0;
- } else {
- totalCases = toInt(user.disputes?.length);
- ruledCases = toInt(user.totalResolvedDisputes);
- }
+const TABS = [
+ { text: "Stakes", value: 0, Icon: PnkIcon, path: "stakes/1" },
+ { text: "Cases", value: 1, Icon: DocIcon, path: "cases/1/desc/all" },
+ { text: "Votes", value: 2, Icon: VotedIcon, path: "votes/1/desc/all" },
+];
- return {
- totalCases,
- ruledCases,
- };
+const getTabIndex = (currentPath: string) => {
+ return TABS.findIndex((tab) => currentPath.includes(tab.path.split("/")[0]));
};
const Profile: React.FC = () => {
const { isConnected, address: connectedAddress } = useAccount();
- const { page, order, filter } = useParams();
const [searchParams] = useSearchParams();
- const location = useRootPath();
+ const { pathname } = useLocation();
const navigate = useNavigate();
const searchParamAddress = searchParams.get("address")?.toLowerCase();
- const addressToQuery = searchParamAddress || connectedAddress?.toLowerCase();
- const casesPerPage = 3;
- const pageNumber = parseInt(page ?? "1");
- const disputeSkip = casesPerPage * (pageNumber - 1);
- const decodedFilter = decodeURIFilter(filter ?? "all");
- const { data: disputesData } = useMyCasesQuery(
- addressToQuery,
- disputeSkip,
- decodedFilter,
- order === "asc" ? OrderDirection.Asc : OrderDirection.Desc
- );
- const { data: userData } = useUserQuery(addressToQuery, decodedFilter);
- const { totalCases, ruledCases: totalResolvedCases } = useMemo(
- () => calculateStats(userData?.user, decodedFilter),
- [userData?.user, decodedFilter]
- );
- const totalPages = useMemo(
- () => (!isUndefined(totalCases) ? Math.ceil(totalCases / casesPerPage) : 1),
- [totalCases, casesPerPage]
- );
+
+ useEffect(() => {
+ if (isConnected && !searchParamAddress && connectedAddress) {
+ navigate(`${pathname}?address=${connectedAddress.toLowerCase()}`, { replace: true });
+ }
+ }, [isConnected, searchParamAddress, connectedAddress, pathname, navigate]);
+
+ const handleTabChange = (tabIndex: number) => {
+ const selectedTab = TABS[tabIndex];
+ const basePath = `/profile/${selectedTab.path}`;
+ const queryParam = searchParamAddress ? `?address=${searchParamAddress}` : "";
+ navigate(`${basePath}${queryParam}`);
+ };
return (
- {isConnected || searchParamAddress ? (
+ {searchParamAddress ? (
<>
-
-
-
- navigate(`${location}/${newPage}/${order}/${filter}?${searchParams.toString()}`)
- }
- {...{ casesPerPage }}
+
+ handleTabChange(tabIndex)}
/>
+
+ } />
+ } />
+ } />
+
+ }
+ />
+
>
- ) : (
+ ) : !isConnected ? (
To see your profile, connect first
- )}
+ ) : null}
diff --git a/web/src/utils/findCourtNameById.ts b/web/src/utils/findCourtNameById.ts
new file mode 100644
index 000000000..b5cbadfdd
--- /dev/null
+++ b/web/src/utils/findCourtNameById.ts
@@ -0,0 +1,14 @@
+import { CourtTreeQuery } from "src/graphql/graphql";
+
+export const findCourtNameById = (courtTreeData: CourtTreeQuery, courtId: string) => {
+ const traverse = (court: CourtTreeQuery["court"]) => {
+ if (court.id === courtId) return court.name;
+ for (const child of court.children) {
+ const found = traverse(child);
+ if (found) return found;
+ }
+ return null;
+ };
+
+ return traverse(courtTreeData.court) ?? undefined;
+};
diff --git a/web/src/utils/userLevelCalculation.ts b/web/src/utils/userLevelCalculation.ts
index c5d1424a5..9522919f2 100644
--- a/web/src/utils/userLevelCalculation.ts
+++ b/web/src/utils/userLevelCalculation.ts
@@ -1,4 +1,4 @@
-interface ILevelCriteria {
+export interface ILevelCriteria {
level: number;
title: string;
minCoherenceScore: number;