diff --git a/web/src/components/CasesDisplay/index.tsx b/web/src/components/CasesDisplay/index.tsx index 39dc25ec4..385a0beab 100644 --- a/web/src/components/CasesDisplay/index.tsx +++ b/web/src/components/CasesDisplay/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import styled from "styled-components"; import { useLocation } from "react-router-dom"; +import { useAccount } from "wagmi"; import ArrowIcon from "svgs/icons/arrow.svg"; @@ -29,6 +30,12 @@ const StyledLabel = styled.label` font-size: ${responsiveSize(14, 16)}; `; +const LinksContainer = styled.div` + display: flex; + flex-direction: row; + gap: 16px; +`; + interface ICasesDisplay extends ICasesGrid { numberDisputes?: number; numberClosedDisputes?: number; @@ -48,15 +55,25 @@ const CasesDisplay: React.FC = ({ totalPages, }) => { const location = useLocation(); + const { isConnected, address } = useAccount(); + const profileLink = isConnected && address ? `/profile/1/desc/all?address=${address}` : null; + return (
{title} - {location.pathname.startsWith("/cases/display/1/desc/all") ? ( - - Create a case - - ) : null} + + {location.pathname.startsWith("/cases/display") && profileLink ? ( + + My Cases + + ) : null} + {location.pathname.startsWith("/cases/display") ? ( + + Create a case + + ) : null} + diff --git a/web/src/components/ConnectWallet/AccountDisplay.tsx b/web/src/components/ConnectWallet/AccountDisplay.tsx index bc44bafc7..a8b74cb31 100644 --- a/web/src/components/ConnectWallet/AccountDisplay.tsx +++ b/web/src/components/ConnectWallet/AccountDisplay.tsx @@ -16,7 +16,6 @@ const Container = styled.div` flex-direction: column; justify-content: space-between; height: auto; - align-items: flex-start; gap: 8px; align-items: center; background-color: ${({ theme }) => theme.whiteBackground}; @@ -101,10 +100,8 @@ const ChainConnectionContainer = styled.div` const StyledIdenticon = styled(Identicon)<{ size: `${number}` }>` align-items: center; - svg { - width: ${({ size }) => size + "px"}; - height: ${({ size }) => size + "px"}; - } + width: ${({ size }) => size + "px"} !important; + height: ${({ size }) => size + "px"} !important; `; const StyledAvatar = styled.img<{ size: `${number}` }>` @@ -115,12 +112,16 @@ const StyledAvatar = styled.img<{ size: `${number}` }>` height: ${({ size }) => size + "px"}; `; +const StyledSmallLabel = styled.label` + font-size: 14px !important; +`; + interface IIdenticonOrAvatar { size?: `${number}`; address?: `0x${string}`; } -export const IdenticonOrAvatar: React.FC = ({ size = "16", address: propAddress }) => { +export const IdenticonOrAvatar: React.FC = ({ size = "20", address: propAddress }) => { const { address: defaultAddress } = useAccount(); const address = propAddress || defaultAddress; @@ -142,9 +143,10 @@ export const IdenticonOrAvatar: React.FC = ({ size = "16", a interface IAddressOrName { address?: `0x${string}`; + smallDisplay?: boolean; } -export const AddressOrName: React.FC = ({ address: propAddress }) => { +export const AddressOrName: React.FC = ({ address: propAddress, smallDisplay }) => { const { address: defaultAddress } = useAccount(); const address = propAddress || defaultAddress; @@ -153,7 +155,9 @@ export const AddressOrName: React.FC = ({ address: propAddress } chainId: 1, }); - return ; + const content = data ?? (isAddress(address!) ? shortenAddress(address) : address); + + return smallDisplay ? {content} : ; }; export const ChainDisplay: React.FC = () => { @@ -166,7 +170,7 @@ const AccountDisplay: React.FC = () => { return ( - + diff --git a/web/src/hooks/queries/useTopStakedJurorsByCourt.ts b/web/src/hooks/queries/useTopStakedJurorsByCourt.ts new file mode 100644 index 000000000..8784f0b79 --- /dev/null +++ b/web/src/hooks/queries/useTopStakedJurorsByCourt.ts @@ -0,0 +1,60 @@ +import { useQuery } from "@tanstack/react-query"; +import { useGraphqlBatcher } from "context/GraphqlBatcher"; +import { graphql } from "src/graphql"; +import { TopStakedJurorsByCourtQuery, OrderDirection } from "src/graphql/graphql"; + +const topStakedJurorsByCourtQuery = graphql(` + query TopStakedJurorsByCourt( + $courtId: ID! + $skip: Int + $first: Int + $orderBy: JurorTokensPerCourt_orderBy + $orderDirection: OrderDirection + $search: String + ) { + jurorTokensPerCourts( + where: { court_: { id: $courtId }, effectiveStake_gt: 0, juror_: { userAddress_contains_nocase: $search } } + skip: $skip + first: $first + orderBy: $orderBy + orderDirection: $orderDirection + ) { + court { + id + } + juror { + id + userAddress + } + effectiveStake + } + } +`); + +export const useTopStakedJurorsByCourt = ( + courtId: string, + skip: number, + first: number, + orderBy: string, + orderDirection: OrderDirection, + search = "" +) => { + const { graphqlBatcher } = useGraphqlBatcher(); + return useQuery({ + queryKey: ["TopStakedJurorsByCourt", courtId, skip, first, orderBy, orderDirection, search], + staleTime: 10 * 60 * 1000, + queryFn: () => + graphqlBatcher.fetch({ + id: crypto.randomUUID(), + document: topStakedJurorsByCourtQuery, + variables: { + courtId, + skip, + first, + orderBy, + orderDirection, + search: search.toLowerCase(), + }, + }), + }); +}; diff --git a/web/src/layout/Header/navbar/Explore.tsx b/web/src/layout/Header/navbar/Explore.tsx index 8c4960d7a..ab544381b 100644 --- a/web/src/layout/Header/navbar/Explore.tsx +++ b/web/src/layout/Header/navbar/Explore.tsx @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useMemo } from "react"; import styled, { css } from "styled-components"; import { landscapeStyle } from "styles/landscapeStyle"; import { Link, useLocation } from "react-router-dom"; +import { useAccount } from "wagmi"; import { useOpenContext } from "../MobileHeader"; @@ -50,14 +51,6 @@ const StyledLink = styled(Link)<{ isActive: boolean; isMobileNavbar?: boolean }> )}; `; -const links = [ - { to: "/", text: "Home" }, - { to: "/cases/display/1/desc/all", text: "Cases" }, - { to: "/courts", text: "Courts" }, - { to: "/jurors/1/desc/all", text: "Jurors" }, - { to: "/get-pnk", text: "Get PNK" }, -]; - interface IExplore { isMobileNavbar?: boolean; } @@ -65,20 +58,34 @@ interface IExplore { const Explore: React.FC = ({ isMobileNavbar }) => { const location = useLocation(); const { toggleIsOpen } = useOpenContext(); + const { isConnected, address } = useAccount(); + + const navLinks = useMemo(() => { + const base = [ + { to: "/", text: "Home" }, + { to: "/cases/display/1/desc/all", text: "Cases" }, + { to: "/courts", text: "Courts" }, + { to: "/jurors/1/desc/all", text: "Jurors" }, + { to: "/get-pnk", text: "Get PNK" }, + ]; + if (isConnected && address) { + base.push({ to: `/profile/1/desc/all?address=${address}`, text: "My Profile" }); + } + return base; + }, [isConnected, address]); return ( Explore - {links.map(({ to, text }) => ( - - {text} - - ))} + {navLinks.map(({ to, text }) => { + const pathBase = to.split("?")[0]; + const isActive = pathBase === "/" ? location.pathname === "/" : location.pathname.startsWith(pathBase); + return ( + + {text} + + ); + })} ); }; diff --git a/web/src/pages/Courts/CourtDetails/Description.tsx b/web/src/pages/Courts/CourtDetails/Description.tsx index 6cae460bc..b8ff78dbb 100644 --- a/web/src/pages/Courts/CourtDetails/Description.tsx +++ b/web/src/pages/Courts/CourtDetails/Description.tsx @@ -2,8 +2,7 @@ import React, { useEffect } from "react"; import styled from "styled-components"; import ReactMarkdown from "react-markdown"; -import { Routes, Route, Navigate, useParams, useNavigate, useLocation } from "react-router-dom"; - +import { Routes, Route, Navigate, useParams, useNavigate, useLocation, useSearchParams } from "react-router-dom"; import { Tabs } from "@kleros/ui-components-library"; import { useCourtPolicy } from "queries/useCourtPolicy"; @@ -97,38 +96,34 @@ const Description: React.FC = () => { const { data: policy } = useCourtPolicy(id); const navigate = useNavigate(); const location = useLocation(); + const [searchParams] = useSearchParams(); + const suffix = searchParams.toString() ? `?${searchParams.toString()}` : ""; const currentPathName = location.pathname.split("/").at(-1); const filteredTabs = TABS.filter(({ isVisible }) => isVisible(policy)); const currentTab = TABS.findIndex(({ path }) => path === currentPathName); - const handleTabChange = (index: number) => { - navigate(TABS[index].path); + const handleTabChange = (i: number) => { + navigate(`${TABS[i].path}${suffix}`); }; - useEffect(() => { if (currentPathName && !filteredTabs.map((t) => t.path).includes(currentPathName) && filteredTabs.length > 0) { - navigate(filteredTabs[0].path, { replace: true }); + navigate(`${filteredTabs[0].path}${suffix}`, { replace: true }); } - }, [policy, currentPathName, filteredTabs, navigate]); - - return ( - <> - {policy ? ( - - - - - - - - 0 ? filteredTabs[0].path : ""} replace />} /> - - - - ) : null} - - ); + }, [policy, currentPathName, filteredTabs, navigate, suffix]); + return policy ? ( + + + + + + + + 0 ? filteredTabs[0].path : ""} replace />} /> + + + + ) : null; }; const formatMarkdown = (markdown?: string) => diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Header.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Header.tsx new file mode 100644 index 000000000..01ebc8dc0 --- /dev/null +++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Header.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import styled from "styled-components"; + +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: 18px 24px; + justify-content: space-between; + margin-top: ${responsiveSize(12, 16)}; +`; + +const StyledLabel = styled.label` + font-size: 14px; + color: ${({ theme }) => theme.secondaryText}; +`; + +const Header: React.FC = () => { + return ( + + Juror + PNK Staked + + ); +}; + +export default Header; diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/JurorCard.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/JurorCard.tsx new file mode 100644 index 000000000..dafa1849e --- /dev/null +++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/JurorCard.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styled from "styled-components"; + +import { hoverShortTransitionTiming } from "styles/commonStyles"; + +import JurorTitle from "pages/Home/TopJurors/JurorCard/JurorTitle"; +import Stake from "./Stake"; + +const Container = styled.div` + ${hoverShortTransitionTiming} + display: flex; + justify-content: space-between; + width: 100%; + background-color: ${({ theme }) => theme.whiteBackground}; + border: 1px solid ${({ theme }) => theme.stroke}; + border-top: none; + align-items: center; + padding: 18px 24px; + + :hover { + background-color: ${({ theme }) => theme.lightGrey}BB; + } +`; + +interface IJurorCard { + address: string; + effectiveStake: string; +} + +const JurorCard: React.FC = ({ address, effectiveStake }) => { + return ( + + + + + ); +}; + +export default JurorCard; diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Stake.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Stake.tsx new file mode 100644 index 000000000..5d2a53c0b --- /dev/null +++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/Stake.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import styled from "styled-components"; + +import { formatPNK } from "utils/format"; + +interface IStake { + effectiveStake: string; +} + +const StyledLabel = styled.label` + font-size: 14px; + color: ${({ theme }) => theme.primaryText}; +`; + +const Stake: React.FC = ({ effectiveStake }) => { + return {formatPNK(BigInt(effectiveStake))} ; +}; +export default Stake; diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/index.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/index.tsx new file mode 100644 index 000000000..f5edbddaa --- /dev/null +++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/DisplayJurors/index.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useParams, useSearchParams } from "react-router-dom"; +import styled from "styled-components"; +import { responsiveSize } from "styles/responsiveSize"; +import { isUndefined } from "utils/index"; +import { useTopStakedJurorsByCourt } from "queries/useTopStakedJurorsByCourt"; +import { OrderDirection } from "src/graphql/graphql"; +import { SkeletonDisputeListItem } from "components/StyledSkeleton"; +import JurorCard from "./JurorCard"; +import Header from "./Header"; +import { ListContainer as BaseListContainer } from "pages/Home/TopJurors"; + +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 DisplayJurors: React.FC = () => { + const { id: courtId, order } = useParams(); + const [searchParams] = useSearchParams(); + const searchValue = searchParams.get("jurorStakedSearch") ?? ""; + const [page, setPage] = useState(0); + const skip = page * PER_PAGE; + const { data, isFetching } = useTopStakedJurorsByCourt( + courtId, + skip, + PER_PAGE, + "effectiveStake", + order === "asc" ? OrderDirection.Asc : OrderDirection.Desc, + searchValue + ); + const [acc, setAcc] = useState<{ id: string; userAddress: string; effectiveStake: string }[]>([]); + + useEffect(() => { + setPage(0); + setAcc([]); + }, [searchValue, courtId, order]); + + useEffect(() => { + const chunk = + data?.jurorTokensPerCourts?.map((j) => ({ + id: j.juror.id, + userAddress: j.juror.userAddress, + effectiveStake: j.effectiveStake, + })) ?? []; + if (chunk.length) setAcc((prev) => [...prev, ...chunk]); + }, [data]); + + 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 jurors = useMemo(() => acc, [acc]); + + return ( + <> + {!isUndefined(jurors) && jurors.length === 0 && !isFetching ? ( + No jurors found + ) : ( + +
+ + {jurors.map((j) => ( + + ))} + {isFetching && [...Array(9)].map((_, i) => )} +
+ + + )} + + ); +}; + +export default DisplayJurors; diff --git a/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/Search.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/Search.tsx new file mode 100644 index 000000000..1f3b12998 --- /dev/null +++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/Search.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import { Searchbar } from "@kleros/ui-components-library"; +import { useDebounce } from "react-use"; +import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; +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("jurorStakedSearch") ?? ""; + const [value, setValue] = useState(initial); + useDebounce( + () => { + const params = new URLSearchParams(searchParams); + if (isEmpty(value)) { + params.delete("jurorStakedSearch"); + } else { + params.set("jurorStakedSearch", 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/JurorsStakedByCourt/index.tsx b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/index.tsx new file mode 100644 index 000000000..07f1b7079 --- /dev/null +++ b/web/src/pages/Courts/CourtDetails/JurorsStakedByCourt/index.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import styled from "styled-components"; + +import { responsiveSize } from "styles/responsiveSize"; +import Search from "./Search"; +import DisplayJurors from "./DisplayJurors"; + +const Container = styled.div` + margin-top: ${responsiveSize(28, 48)}; + max-width: 578px; +`; + +const Title = styled.h1` + margin-bottom: ${responsiveSize(12, 16)}; + font-size: ${responsiveSize(20, 24)}; +`; + +const JurorsStakedByCourt: React.FC<{ courtName: string | undefined }> = ({ courtName }) => { + return ( + + + Jurors Staked in {courtName} + {courtName?.toLowerCase().endsWith("court") || courtName?.toLowerCase().startsWith("corte") ? null : " Court"} + + + + + ); +}; + +export default JurorsStakedByCourt; diff --git a/web/src/pages/Courts/CourtDetails/Stats/index.tsx b/web/src/pages/Courts/CourtDetails/Stats/index.tsx index 75fb02a14..0ae92d7f2 100644 --- a/web/src/pages/Courts/CourtDetails/Stats/index.tsx +++ b/web/src/pages/Courts/CourtDetails/Stats/index.tsx @@ -16,7 +16,7 @@ import { landscapeStyle } from "styles/landscapeStyle"; import StatsContent from "./StatsContent"; const Container = styled.div` - padding: 12px 24px; + padding: 0 24px 12px 24px; `; const Header = styled.h3` diff --git a/web/src/pages/Courts/CourtDetails/index.tsx b/web/src/pages/Courts/CourtDetails/index.tsx index cb71d1308..93689e50e 100644 --- a/web/src/pages/Courts/CourtDetails/index.tsx +++ b/web/src/pages/Courts/CourtDetails/index.tsx @@ -25,6 +25,7 @@ import Description from "./Description"; import StakePanel from "./StakePanel"; import Stats from "./Stats"; import TopSearch from "./TopSearch"; +import JurorsStakedByCourt from "./JurorsStakedByCourt"; const Container = styled.div``; @@ -147,6 +148,7 @@ const CourtDetails: React.FC = () => { + ); diff --git a/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx b/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx index 437aea26a..4eb87f0c1 100644 --- a/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx +++ b/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "styled-components"; -import ArrowIcon from "svgs/icons/arrow.svg"; +import ArrowSvg from "svgs/icons/arrow.svg"; import { IdenticonOrAvatar, AddressOrName } from "components/ConnectWallet/AccountDisplay"; import { StyledArrowLink } from "components/StyledArrowLink"; @@ -23,7 +23,7 @@ const Container = styled.div` } `; -export const ReStyledArrowLink = styled(StyledArrowLink)` +export const ReStyledArrowLink = styled(StyledArrowLink)<{ smallDisplay?: boolean }>` label { cursor: pointer; color: ${({ theme }) => theme.primaryBlue}; @@ -34,13 +34,23 @@ export const ReStyledArrowLink = styled(StyledArrowLink)` color: ${({ theme }) => theme.secondaryBlue}; } } + + ${({ smallDisplay }) => + smallDisplay && + ` + > svg { + height: 14.5px; + width: 14.5px; + } + `} `; interface IJurorTitle { address: string; + smallDisplay?: boolean; } -const JurorTitle: React.FC = ({ address }) => { +const JurorTitle: React.FC = ({ address, smallDisplay }) => { const { isConnected, address: connectedAddress } = useAccount(); const profileLink = isConnected && connectedAddress?.toLowerCase() === address.toLowerCase() @@ -49,10 +59,10 @@ const JurorTitle: React.FC = ({ address }) => { return ( - - - - + + + + );