diff --git a/web/src/assets/pngs/dashboard/aristoteles.png b/web/src/assets/pngs/dashboard/aristoteles.png index 98710b0f2..9bc2f76fc 100644 Binary files a/web/src/assets/pngs/dashboard/aristoteles.png and b/web/src/assets/pngs/dashboard/aristoteles.png differ diff --git a/web/src/assets/pngs/dashboard/diogenes.png b/web/src/assets/pngs/dashboard/diogenes.png index 1c326ca1d..74afcc18b 100644 Binary files a/web/src/assets/pngs/dashboard/diogenes.png and b/web/src/assets/pngs/dashboard/diogenes.png differ diff --git a/web/src/assets/pngs/dashboard/plato.png b/web/src/assets/pngs/dashboard/plato.png index 7fb43789e..5b0ec9a87 100644 Binary files a/web/src/assets/pngs/dashboard/plato.png and b/web/src/assets/pngs/dashboard/plato.png differ diff --git a/web/src/assets/pngs/dashboard/pythagoras.png b/web/src/assets/pngs/dashboard/pythagoras.png index 7a4e49957..327b1ea1b 100644 Binary files a/web/src/assets/pngs/dashboard/pythagoras.png and b/web/src/assets/pngs/dashboard/pythagoras.png differ diff --git a/web/src/assets/pngs/dashboard/socrates.png b/web/src/assets/pngs/dashboard/socrates.png index 6350b40f0..81d85195c 100644 Binary files a/web/src/assets/pngs/dashboard/socrates.png and b/web/src/assets/pngs/dashboard/socrates.png differ diff --git a/web/src/assets/svgs/menu-icons/help.svg b/web/src/assets/svgs/menu-icons/help.svg index 71c5d151e..1c89e1fcf 100644 --- a/web/src/assets/svgs/menu-icons/help.svg +++ b/web/src/assets/svgs/menu-icons/help.svg @@ -1,3 +1,3 @@ - - + + diff --git a/web/src/components/Popup/MiniGuides/Level.tsx b/web/src/components/Popup/MiniGuides/Level.tsx new file mode 100644 index 000000000..5e9c94ad4 --- /dev/null +++ b/web/src/components/Popup/MiniGuides/Level.tsx @@ -0,0 +1,124 @@ +import React, { useState } from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import { Card as _Card } from "@kleros/ui-components-library"; +import PixelArt from "pages/Dashboard/JurorInfo/PixelArt"; +import Coherency from "pages/Dashboard/JurorInfo/Coherency"; +import Template from "./Template"; + +const Card = styled(_Card)` + display: flex; + flex-direction: column; + align-items: center; + width: 234px; + height: 100%; + gap: 28px; + + padding: 24px; + + ${landscapeStyle( + () => css` + flex-direction: row; + width: 100%; + height: 236px; + ` + )} +`; + +const Title = styled.h1` + margin-bottom: 0; +`; + +const LeftContentContainer = styled.div` + display: flex; + gap: 18px; + flex-direction: column; +`; + +const userLevelData = [ + { + level: 1, + title: "Pythagoras", + totalCoherent: 6, + totalResolvedDisputes: 10, + firstParagraph: "Jurors are classified into distinct levels according to their performance starting from Level 1.", + secondParagraph: + "Level 1: Jurors with 0 cases arbitrated, OR Jurors with ≥ 1 case arbitrated with 0-70% of coherent votes.", + }, + { + level: 2, + title: "Socrates", + totalCoherent: 7, + totalResolvedDisputes: 10, + firstParagraph: "Level 2: Jurors with ≥ 3 cases arbitrated with 70%-80% of coherent votes.", + }, + { + level: 3, + title: "Plato", + totalCoherent: 8, + totalResolvedDisputes: 10, + firstParagraph: "Level 3: Jurors with ≥ 7 cases arbitrated with 80%-90% of coherent votes.", + }, + { + level: 4, + title: "Aristoteles", + totalCoherent: 9, + totalResolvedDisputes: 10, + firstParagraph: "Level 4: Jurors with ≥ 10 cases arbitrated with more than 90% of coherent votes.", + }, + { + level: 0, + title: "Diogenes", + totalCoherent: 3, + totalResolvedDisputes: 10, + firstParagraph: + "There's a level for the low-performance/lazy jurors. Level 0: Jurors with ≥ 3 cases arbitrated with less than 50% of coherent votes.", + }, +]; + +const LeftContent: React.FC<{ currentPage: number }> = ({ currentPage }) => { + return ( + + + Juror Level {userLevelData[currentPage - 1].level}: {userLevelData[currentPage - 1].title} + + + + + ); +}; + +const RightContent: React.FC<{ currentPage: number }> = ({ currentPage }) => { + return ( + + + + + ); +}; + +interface ILevel { + toggleIsLevelMiniGuidesOpen: () => void; +} + +const Level: React.FC = ({ toggleIsLevelMiniGuidesOpen }) => { + const [currentPage, setCurrentPage] = useState(1); + + return ( + } + RightContent={} + onClose={toggleIsLevelMiniGuidesOpen} + currentPage={currentPage} + setCurrentPage={setCurrentPage} + numPages={userLevelData.length} + /> + ); +}; + +export default Level; diff --git a/web/src/components/Popup/MiniGuides/Template.tsx b/web/src/components/Popup/MiniGuides/Template.tsx new file mode 100644 index 000000000..d8bcb18a5 --- /dev/null +++ b/web/src/components/Popup/MiniGuides/Template.tsx @@ -0,0 +1,155 @@ +import React, { Dispatch, SetStateAction, useRef } from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import { CompactPagination } from "@kleros/ui-components-library"; +import { Overlay } from "components/Overlay"; +import BookOpenIcon from "tsx:assets/svgs/icons/book-open.svg"; +import { useFocusOutside } from "hooks/useFocusOutside"; + +const Container = styled.div` + display: flex; + margin: 0 auto; + width: auto; + z-index: 10; + position: fixed; + width: 82vw; + flex-direction: column; + + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-height: 80vh; + overflow-y: auto; + + ${landscapeStyle( + () => css` + overflow-y: hidden; + width: calc(700px + (900 - 700) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + flex-direction: row; + height: 500px; + ` + )} +`; + +const LeftContainer = styled.div` + display: grid; + grid-template-rows: auto 1fr auto; + width: 82vw; + min-height: 356px; + padding: calc(24px + (32 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + background-color: ${({ theme }) => theme.whiteBackground}; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + + ${landscapeStyle( + () => css` + overflow-y: hidden; + width: calc(350px + (450 - 350) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + height: 500px; + min-height: auto; + ` + )} +`; + +const HowItWorks = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-bottom: calc(32px + (64 - 32) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + + label { + color: ${({ theme }) => theme.secondaryPurple}; + } +`; + +const StyledCompactPagination = styled(CompactPagination)` + align-self: end; + justify-self: end; +`; + +const Close = styled.label` + position: absolute; + top: calc(24px + (32 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + right: 17px; + display: flex; + align-items: flex-end; + justify-content: flex-end; + cursor: pointer; + + color: ${({ theme }) => theme.primaryBlue}; + + ${landscapeStyle( + () => css` + z-index: 11; + ` + )} +`; + +const RightContainer = styled.div` + width: 82vw; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: calc(24px + (32 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875) 17px; + background-color: ${({ theme }) => theme.mediumBlue}; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + height: 800px; + + ${landscapeStyle( + () => css` + overflow-y: hidden; + width: calc(350px + (450 - 350) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + height: 500px; + ` + )} +`; + +interface ITemplate { + onClose: () => void; + LeftContent: React.ReactNode; + RightContent: React.ReactNode; + currentPage: number; + setCurrentPage: Dispatch>; + numPages: number; +} + +const Template: React.FC = ({ + onClose, + LeftContent, + RightContent, + currentPage, + setCurrentPage, + numPages, +}) => { + const containerRef = useRef(null); + useFocusOutside(containerRef, () => { + onClose(); + }); + return ( + <> + + + + + + + + Close + {LeftContent} + + + {RightContent} + + + ); +}; + +export default Template; diff --git a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx index e340065e2..548c03901 100644 --- a/web/src/pages/Dashboard/JurorInfo/Coherency.tsx +++ b/web/src/pages/Dashboard/JurorInfo/Coherency.tsx @@ -24,15 +24,25 @@ const tooltipMsg = interface ICoherency { userLevelData: { - scoreRange: number[]; level: number; title: string; }; totalCoherent: number; totalResolvedDisputes: number; + isMiniGuide: boolean; } -const Coherency: React.FC = ({ userLevelData, totalCoherent, totalResolvedDisputes }) => { +const Coherency: React.FC = ({ userLevelData, totalCoherent, totalResolvedDisputes, isMiniGuide }) => { + const votesContent = ( + + {" "} + {totalCoherent}/{totalResolvedDisputes}{" "} + + + ); + return ( {userLevelData.title} @@ -40,15 +50,13 @@ const Coherency: React.FC = ({ userLevelData, totalCoherent, totalRe - - - + {!isMiniGuide ? ( + + {votesContent} + + ) : ( + votesContent + )} ); }; diff --git a/web/src/pages/Dashboard/JurorInfo/Header.tsx b/web/src/pages/Dashboard/JurorInfo/Header.tsx new file mode 100644 index 000000000..a3eb04a09 --- /dev/null +++ b/web/src/pages/Dashboard/JurorInfo/Header.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import styled from "styled-components"; +import XIcon from "svgs/socialmedia/x.svg"; + +const Container = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +`; + +const StyledTitle = styled.h1` + margin-bottom: calc(16px + (48 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); +`; + +const XLinkContainer = styled.div` + display: flex; + color: ${({ theme }) => theme.primaryBlue}; + margin-bottom: calc(16px + (48 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); +`; + +const StyledXIcon = styled(XIcon)` + fill: ${({ theme }) => theme.primaryBlue}; + width: 16px; + height: 16px; +`; + +const StyledLink = styled.a` + display: flex; + border: 0px; + align-items: center; + gap: 8px; + + &:hover { + text-decoration: underline; + } +`; + +interface IHeader { + levelTitle: string; + levelNumber: number; + totalCoherent: number; + totalResolvedDisputes: number; +} + +const Header: React.FC = ({ levelTitle, levelNumber, totalCoherent, totalResolvedDisputes }) => { + const coherencePercentage = parseFloat(((totalCoherent / Math.max(totalResolvedDisputes, 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: ${totalCoherent}/${totalResolvedDisputes}\n\nBe a juror with me! ➡️ ${courtUrl}`; + const xShareUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(xPostText)}`; + + return ( + + Juror Dashboard + + + Share your juror score + + + + ); +}; + +export default Header; diff --git a/web/src/pages/Dashboard/JurorInfo/index.tsx b/web/src/pages/Dashboard/JurorInfo/index.tsx index e0dc87b2e..84eda39b9 100644 --- a/web/src/pages/Dashboard/JurorInfo/index.tsx +++ b/web/src/pages/Dashboard/JurorInfo/index.tsx @@ -2,6 +2,7 @@ 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 Header from "./Header"; import Coherency from "./Coherency"; import JurorRewards from "./JurorRewards"; import PixelArt from "./PixelArt"; @@ -12,10 +13,6 @@ import { getUserLevelData } from "utils/userLevelCalculation"; const Container = styled.div``; -const Header = styled.h1` - margin-bottom: calc(16px + (48 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); -`; - const Card = styled(_Card)` display: flex; flex-direction: column; @@ -47,13 +44,19 @@ const JurorInfo: React.FC = () => { return ( -
Juror Dashboard
+
diff --git a/web/src/pages/Dashboard/WithHelpTooltip.tsx b/web/src/pages/Dashboard/WithHelpTooltip.tsx index 976d85b4b..3a4fddb39 100644 --- a/web/src/pages/Dashboard/WithHelpTooltip.tsx +++ b/web/src/pages/Dashboard/WithHelpTooltip.tsx @@ -1,5 +1,6 @@ import React from "react"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; import { Tooltip } from "@kleros/ui-components-library"; import _HelpIcon from "svgs/menu-icons/help.svg"; @@ -9,8 +10,17 @@ const Container = styled.div` `; const HelpIcon = styled(_HelpIcon)` + height: 12px; + width: 12px; fill: ${({ theme }) => theme.secondaryText}; - margin: 4px; + margin: 4px 4px 6px 8px; + + ${landscapeStyle( + () => css` + height: 14px; + width: 14px; + ` + )} `; interface IWithHelpTooltip { diff --git a/web/src/pages/Home/TopJurors/Header/Coherency.tsx b/web/src/pages/Home/TopJurors/Header/Coherency.tsx new file mode 100644 index 000000000..832793291 --- /dev/null +++ b/web/src/pages/Home/TopJurors/Header/Coherency.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { BREAKPOINT_LANDSCAPE, landscapeStyle } from "styles/landscapeStyle"; +import { useWindowSize } from "react-use"; +import WithHelpTooltip from "pages/Dashboard/WithHelpTooltip"; + +const Container = styled.div` + display: flex; + font-size: 12px !important; + &::before { + content: "Votes"; + } + color: ${({ theme }) => theme.secondaryText}; + align-items: center; + + ${landscapeStyle( + () => + css` + font-size: 14px !important; + &::before { + content: "Coherent Votes"; + } + ` + )} +`; + +const coherentVotesTooltipMsg = + "This is the ratio of coherent votes made by a juror: " + + "the number in the left is the number of times where the juror " + + "voted coherently and the number in the right is the total number of times " + + "the juror voted"; + +const Coherency: React.FC = () => { + const { width } = useWindowSize(); + return ( + + BREAKPOINT_LANDSCAPE ? "top" : "left"} + tooltipMsg={coherentVotesTooltipMsg} + > + + ); +}; +export default Coherency; diff --git a/web/src/pages/Home/TopJurors/Header/HowItWorks.tsx b/web/src/pages/Home/TopJurors/Header/HowItWorks.tsx new file mode 100644 index 000000000..36cd00bf0 --- /dev/null +++ b/web/src/pages/Home/TopJurors/Header/HowItWorks.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import styled from "styled-components"; +import { useToggle } from "react-use"; +import BookOpenIcon from "tsx:assets/svgs/icons/book-open.svg"; +import Level from "components/Popup/MiniGuides/Level"; + +const Container = styled.div` + display: flex; + align-items: center; + gap: 8px; + + &, + & * { + cursor: pointer; + } + + svg { + path { + fill: ${({ theme }) => theme.primaryBlue}; + } + } +`; + +const StyledLabel = styled.label` + color: ${({ theme }) => theme.primaryBlue}; +`; + +const HowItWorks: React.FC = () => { + const [isLevelMiniGuidesOpen, toggleIsLevelMiniGuidesOpen] = useToggle(false); + return ( + <> + toggleIsLevelMiniGuidesOpen()}> + + How it works + + {isLevelMiniGuidesOpen && } + + ); +}; + +export default HowItWorks; diff --git a/web/src/pages/Home/TopJurors/Header/JurorTitle.tsx b/web/src/pages/Home/TopJurors/Header/JurorTitle.tsx new file mode 100644 index 000000000..ad7ed62e1 --- /dev/null +++ b/web/src/pages/Home/TopJurors/Header/JurorTitle.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import styled from "styled-components"; + +const StyledLabel = styled.label` + width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875); +`; + +const JurorTitle: React.FC = () => Juror; +export default JurorTitle; diff --git a/web/src/pages/Home/TopJurors/Header/Rank.tsx b/web/src/pages/Home/TopJurors/Header/Rank.tsx new file mode 100644 index 000000000..e4f279a8f --- /dev/null +++ b/web/src/pages/Home/TopJurors/Header/Rank.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import styled from "styled-components"; + +const StyledLabel = styled.label` + width: calc(16px + (24 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); +`; + +const Rank: React.FC = () => #; + +export default Rank; diff --git a/web/src/pages/Home/TopJurors/Header/Rewards.tsx b/web/src/pages/Home/TopJurors/Header/Rewards.tsx new file mode 100644 index 000000000..02b062e87 --- /dev/null +++ b/web/src/pages/Home/TopJurors/Header/Rewards.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import WithHelpTooltip from "pages/Dashboard/WithHelpTooltip"; + +const Container = styled.div` + display: flex; + font-size: 12px !important; + &::before { + content: "Rewards"; + } + color: ${({ theme }) => theme.secondaryText}; + align-items: center; + + ${landscapeStyle( + () => + css` + width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + + font-size: 14px !important; + &::before { + content: "Total Rewards"; + } + ` + )} +`; + +const totalRewardsTooltipMsg = + "Users have an economic interest in serving as jurors in Kleros: " + + "collecting the Juror Rewards in exchange for their work. Each juror who " + + "is coherent with the final ruling receive the Juror Rewards composed of " + + "arbitration fees (ETH) + PNK redistribution between jurors."; + +const Rewards: React.FC = () => ( + + + +); + +export default Rewards; diff --git a/web/src/pages/Home/TopJurors/Header/index.tsx b/web/src/pages/Home/TopJurors/Header/index.tsx new file mode 100644 index 000000000..005681e04 --- /dev/null +++ b/web/src/pages/Home/TopJurors/Header/index.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import Rank from "./Rank"; +import JurorTitle from "./JurorTitle"; +import Rewards from "./Rewards"; +import Coherency from "./Coherency"; +import HowItWorks from "./HowItWorks"; + +const Container = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + background-color: ${({ theme }) => theme.lightBlue}; + padding: 24px; + border 1px solid ${({ theme }) => theme.stroke}; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom: none; + flex-wrap: wrap; + + ${landscapeStyle( + () => + css` + border-bottom: 1px solid ${({ theme }) => theme.stroke}; + padding: 18.6px 32px; + ` + )} +`; + +const PlaceAndTitleAndRewardsAndCoherency = styled.div` + display: none; + + ${landscapeStyle( + () => + css` + display: flex; + flex-direction: row; + gap: calc(20px + (28 - 20) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + align-items: center; + ` + )} +`; + +const StyledLabel = styled.label` + ${landscapeStyle( + () => + css` + display: none; + ` + )} +`; + +const Header: React.FC = () => { + return ( + + Ranking + + + + + + + toggleIsLevelMiniGuidesOpen()} /> + + ); +}; + +export default Header; diff --git a/web/src/pages/Home/TopJurors/JurorCard.tsx b/web/src/pages/Home/TopJurors/JurorCard.tsx deleted file mode 100644 index 558212f5e..000000000 --- a/web/src/pages/Home/TopJurors/JurorCard.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import React from "react"; -import styled, { css } from "styled-components"; -import { landscapeStyle } from "styles/landscapeStyle"; -import { IdenticonOrAvatar, AddressOrName } from "components/ConnectWallet/AccountDisplay"; -import EthIcon from "assets/svgs/icons/eth.svg"; -import PnkIcon from "assets/svgs/icons/kleros.svg"; -import PixelArt from "pages/Dashboard/JurorInfo/PixelArt"; -import { getFormattedRewards } from "utils/jurorRewardConfig"; -import { getUserLevelData } from "utils/userLevelCalculation"; -import { useUserQuery } from "hooks/queries/useUser"; - -const Container = styled.div` - display: flex; - justify-content: space-between; - flex-wrap: wrap; - width: 100%; - height: 100%; - background-color: ${({ theme }) => theme.whiteBackground}; - padding: 24px; - border 1px solid ${({ theme }) => theme.stroke}; - border-top: none; - align-items: center; - - label { - font-size: 16px; - } - - ${landscapeStyle( - () => css` - gap: 0px; - padding: 15.55px 32px; - flex-wrap: nowrap; - ` - )} -`; - -const LogoAndAddress = styled.div` - display: flex; - gap: 10px; - align-items: center; - - canvas { - width: 20px; - height: 20px; - border-radius: 10%; - } -`; - -const PlaceAndTitleAndRewardsAndCoherency = styled.div` - display: flex; - flex-direction: column; - gap: 12px; - - ${landscapeStyle( - () => - css` - flex-direction: row; - gap: 32px; - ` - )} -`; - -const JurorPlace = styled.div` - width: 100%; - - label::before { - content: "#"; - display: inline; - } - - ${landscapeStyle( - () => css` - width: calc(16px + (24 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); - label::before { - display: none; - } - ` - )} -`; - -const JurorTitle = styled.div` - display: flex; - gap: 16px; - align-items: center; - justify-content: flex-start; - - ${landscapeStyle( - () => css` - width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875); - gap: 36px; - ` - )} -`; - -const Rewards = styled.div` - display: flex; - gap: 8px; - align-items: center; - label { - font-weight: 600; - } - width: 164px; - flex-wrap: wrap; - - ${landscapeStyle( - () => - css` - width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875); - ` - )} -`; - -const Coherency = styled.div` - display: flex; - align-items: center; - label { - font-weight: 600; - } - flex-wrap: wrap; -`; - -const StyledIcon = styled.div` - width: 16px; - height: 16px; - - path { - fill: ${({ theme }) => theme.secondaryPurple}; - } -`; - -const HowItWorks = styled.div` - display: flex; - align-items: center; - gap: 16px; -`; - -const StyledIdenticonOrAvatar = styled(IdenticonOrAvatar)``; - -interface IJurorCard { - rank: number; - address: `0x${string}`; - coherenceScore: number; - totalCoherent: number; - totalResolvedDisputes: number; -} - -const JurorCard: React.FC = ({ rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }) => { - const { data } = useUserQuery(address?.toLowerCase()); - - const coherenceRatio = `${totalCoherent}/${totalResolvedDisputes}`; - const userLevelData = getUserLevelData(coherenceScore); - - const formattedRewards = getFormattedRewards(data, {}); - const ethReward = formattedRewards.find((r) => r.token === "ETH")?.amount; - const pnkReward = formattedRewards.find((r) => r.token === "PNK")?.amount; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default JurorCard; diff --git a/web/src/pages/Home/TopJurors/JurorCard/Coherency.tsx b/web/src/pages/Home/TopJurors/JurorCard/Coherency.tsx new file mode 100644 index 000000000..e2b02d044 --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/Coherency.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + align-items: center; + font-weight: 600; + color: ${({ theme }) => theme.primaryText}; + flex-wrap: wrap; +`; + +interface ICoherency { + totalCoherent: number; + totalResolvedDisputes: number; +} + +const Coherency: React.FC = ({ totalCoherent, totalResolvedDisputes }) => { + const coherenceRatio = `${totalCoherent}/${totalResolvedDisputes}`; + + return {coherenceRatio}; +}; + +export default Coherency; diff --git a/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx b/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx new file mode 100644 index 000000000..66079c866 --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/DesktopCard.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { landscapeStyle } from "styles/landscapeStyle"; +import styled, { css } from "styled-components"; +import Rank from "./Rank"; +import JurorTitle from "./JurorTitle"; +import Rewards from "./Rewards"; +import Coherency from "./Coherency"; +import HowItWorks from "./HowItWorks"; + +const Container = styled.div` + display: none; + + justify-content: space-between; + flex-wrap: wrap; + width: 100%; + background-color: ${({ theme }) => theme.whiteBackground}; + border: 1px solid ${({ theme }) => theme.stroke}; + border-top: none; + align-items: center; + padding: 15.55px 32px; + + label { + font-size: 16px; + } + + ${landscapeStyle( + () => css` + display: flex; + ` + )} +`; + +const PlaceAndTitleAndRewardsAndCoherency = styled.div` + display: flex; + flex-direction: row; + gap: calc(20px + (28 - 20) * (min(max(100vw, 375px), 1250px) - 375px) / 875); +`; + +interface IDesktopCard { + rank: number; + address: string; + totalCoherent: number; + totalResolvedDisputes: number; + coherenceScore: number; +} + +const DesktopCard: React.FC = ({ + rank, + address, + totalCoherent, + totalResolvedDisputes, + coherenceScore, +}) => { + return ( + + + + + + + + + + ); +}; +export default DesktopCard; diff --git a/web/src/pages/Home/TopJurors/JurorCard/HowItWorks.tsx b/web/src/pages/Home/TopJurors/JurorCard/HowItWorks.tsx new file mode 100644 index 000000000..bb92948f3 --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/HowItWorks.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import PixelArt from "pages/Dashboard/JurorInfo/PixelArt"; +import { getUserLevelData } from "utils/userLevelCalculation"; + +const Container = styled.div` + display: flex; + align-items: center; + gap: 8px; + + ${landscapeStyle( + () => css` + gap: 16px; + ` + )} +`; + +const StyledLabel = styled.label` + font-size: 12px !important; + + &::before { + content: "Lv. "; + } + + ${landscapeStyle( + () => css` + font-size: 16px !important; + + &::before { + content: "Level "; + } + ` + )} +`; + +interface IHowItWorks { + coherenceScore: number; +} + +const HowItWorks: React.FC = ({ coherenceScore }) => { + const userLevelData = getUserLevelData(coherenceScore); + const level = userLevelData.level; + + return ( + + {level} + + + ); +}; +export default HowItWorks; diff --git a/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx b/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx new file mode 100644 index 000000000..62624909e --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/JurorTitle.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import { IdenticonOrAvatar, AddressOrName } from "components/ConnectWallet/AccountDisplay"; + +const Container = styled.div` + display: flex; + gap: 8px; + align-items: center; + + label { + font-size: 16px; + } + + canvas { + width: 20px; + height: 20px; + border-radius: 10%; + } + + ${landscapeStyle( + () => css` + width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + ` + )} +`; + +interface IJurorTitle { + address: string; +} + +const JurorTitle: React.FC = ({ address }) => { + return ( + + + + + ); +}; +export default JurorTitle; diff --git a/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx b/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx new file mode 100644 index 000000000..910c5595b --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/MobileCard.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import Coherency from "./Coherency"; +import HowItWorks from "./HowItWorks"; +import JurorTitle from "./JurorTitle"; +import Rank from "./Rank"; +import Rewards from "./Rewards"; +import HeaderRewards from "../Header/Rewards"; +import HeaderCoherency from "../Header/Coherency"; + +const Container = styled.div` + display: flex; + justify-content: space-between; + flex-wrap: wrap; + width: 100%; + background-color: ${({ theme }) => theme.whiteBackground}; + padding: 16px 24px 24px 24px; + border 1px solid ${({ theme }) => theme.stroke}; + border-top: none; + align-items: center; + gap: 20px; + + ${landscapeStyle( + () => css` + display: none; + ` + )} +`; + +const TopSide = styled.div` + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + + align-items: center; +`; + +const RankAndTitle = styled.div` + display: flex; + flex-direction: row; + gap: 8px; +`; + +const HeaderRewardsAndRewards = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const BottomSide = styled.div` + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; +`; + +const HeaderCoherencyAndCoherency = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; + + svg { + margin-right: 0; + } +`; + +interface IMobileCard { + rank: number; + address: string; + coherenceScore: number; + totalCoherent: number; + totalResolvedDisputes: number; +} + +const MobileCard: React.FC = ({ rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }) => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; +export default MobileCard; diff --git a/web/src/pages/Home/TopJurors/JurorCard/Rank.tsx b/web/src/pages/Home/TopJurors/JurorCard/Rank.tsx new file mode 100644 index 000000000..8fd80b5d8 --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/Rank.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; + +const Container = styled.div` + color: ${({ theme }) => theme.primaryText}; + + &::before { + content: "#"; + display: inline; + } + + ${landscapeStyle( + () => css` + width: calc(16px + (24 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + &::before { + display: none; + } + ` + )} +`; + +interface IRank { + rank: number; +} + +const Rank: React.FC = ({ rank }) => { + return {rank}; +}; +export default Rank; diff --git a/web/src/pages/Home/TopJurors/JurorCard/Rewards.tsx b/web/src/pages/Home/TopJurors/JurorCard/Rewards.tsx new file mode 100644 index 000000000..bdf6b7d2a --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/Rewards.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { landscapeStyle } from "styles/landscapeStyle"; +import { getFormattedRewards } from "utils/jurorRewardConfig"; +import EthIcon from "assets/svgs/icons/eth.svg"; +import PnkIcon from "assets/svgs/icons/kleros.svg"; +import { useUserQuery } from "hooks/queries/useUser"; + +const Container = styled.div` + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + + ${landscapeStyle( + () => + css` + width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875); + ` + )} +`; + +const StyledIcon = styled.div` + width: 16px; + height: 16px; + + path { + fill: ${({ theme }) => theme.secondaryPurple}; + } +`; + +const StyledLabel = styled.label` + font-size: 16px; + font-weight: 600; + color: ${({ theme }) => theme.primaryText}; +`; + +interface IRewards { + address: string; +} + +const Rewards: React.FC = ({ address }) => { + const { data: userData } = useUserQuery(address?.toLowerCase()); + const formattedRewards = getFormattedRewards(userData, {}); + const ethReward = formattedRewards.find((r) => r.token === "ETH")?.amount; + const pnkReward = formattedRewards.find((r) => r.token === "PNK")?.amount; + + return ( + + {ethReward} + + + + {pnkReward} + + + ); +}; +export default Rewards; diff --git a/web/src/pages/Home/TopJurors/JurorCard/index.tsx b/web/src/pages/Home/TopJurors/JurorCard/index.tsx new file mode 100644 index 000000000..2b2dfe6a2 --- /dev/null +++ b/web/src/pages/Home/TopJurors/JurorCard/index.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import DesktopCard from "./DesktopCard"; +import MobileCard from "./MobileCard"; + +interface IJurorCard { + rank: number; + address: `0x${string}`; + coherenceScore: number; + totalCoherent: number; + totalResolvedDisputes: number; +} + +const JurorCard: React.FC = ({ rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }) => { + const allProps = { rank, address, coherenceScore, totalCoherent, totalResolvedDisputes }; + + return ( + <> + + + + ); +}; + +export default JurorCard; diff --git a/web/src/pages/Home/TopJurors/TopJurorsHeader.tsx b/web/src/pages/Home/TopJurors/TopJurorsHeader.tsx deleted file mode 100644 index 1130b8580..000000000 --- a/web/src/pages/Home/TopJurors/TopJurorsHeader.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React from "react"; -import styled, { css } from "styled-components"; -import { landscapeStyle } from "styles/landscapeStyle"; -import WithHelpTooltip from "pages/Dashboard/WithHelpTooltip"; -import BookOpenIcon from "tsx:assets/svgs/icons/book-open.svg"; - -const Container = styled.div` - display: flex; - justify-content: space-between; - width: 100%; - height: 100%; - background-color: ${({ theme }) => theme.lightBlue}; - padding: 24px; - border 1px solid ${({ theme }) => theme.stroke}; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - flex-wrap: wrap; - - ${landscapeStyle( - () => - css` - flex-wrap: nowrap; - gap: 0px; - padding: 18.6px 32px; - ` - )} -`; - -const PlaceAndTitleAndRewardsAndCoherency = styled.div` - display: flex; - flex-direction: column; - gap: 8px; - - ${landscapeStyle( - () => - css` - flex-direction: row; - gap: 32px; - ` - )} -`; - -const JurorPlace = styled.div` - width: 100%; - - label { - &::before { - content: "# Rank"; - visibility: visible; - } - } - - ${landscapeStyle( - () => - css` - width: calc(16px + (24 - 16) * (min(max(100vw, 375px), 1250px) - 375px) / 875); - - label { - &::before { - content: "#"; - } - } - ` - )} -`; - -const JurorTitle = styled.div` - display: flex; - gap: 16px; - align-items: center; - - label { - font-weight: 400; - font-size: 14px; - line-height: 19px; - color: ${({ theme }) => theme.secondaryText} !important; - } - - ${landscapeStyle( - () => - css` - width: calc(40px + (220 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875); - gap: 36px; - ` - )} -`; - -const Rewards = styled.div` - ${landscapeStyle( - () => - css` - width: calc(60px + (240 - 60) * (min(max(100vw, 375px), 1250px) - 375px) / 875); - ` - )} -`; - -const Coherency = styled.div``; - -const HowItWorks = styled.div` - display: flex; - align-items: center; - gap: 8px; - - label { - color: ${({ theme }) => theme.primaryBlue}; - } - - svg { - path { - fill: ${({ theme }) => theme.primaryBlue}; - } - } -`; - -const totalRewardsTooltipMsg = - "Users have an economic interest in serving as jurors in Kleros: " + - "collecting the Juror Rewards in exchange for their work. Each juror who " + - "is coherent with the final ruling receive the Juror Rewards composed of " + - "arbitration fees (ETH) + PNK redistribution between jurors."; - -const coherentVotesTooltipMsg = - "This is the ratio of coherent votes made by a juror: " + - "the number in the left is the number of times where the juror " + - "voted coherently and the number in the right is the total number of times " + - "the juror voted"; - -const TopJurorsHeader: React.FC = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default TopJurorsHeader; diff --git a/web/src/pages/Home/TopJurors/index.tsx b/web/src/pages/Home/TopJurors/index.tsx index 043912bbd..c0d77ea43 100644 --- a/web/src/pages/Home/TopJurors/index.tsx +++ b/web/src/pages/Home/TopJurors/index.tsx @@ -1,9 +1,9 @@ import React from "react"; import styled from "styled-components"; import { SkeletonDisputeListItem } from "components/StyledSkeleton"; -import { isUndefined } from "utils/index"; -import TopJurorsHeader from "./TopJurorsHeader"; +import Header from "./Header"; import JurorCard from "./JurorCard"; +import { isUndefined } from "utils/index"; import { useTopUsersByCoherenceScore } from "queries/useTopUsersByCoherenceScore"; const Container = styled.div` @@ -32,7 +32,7 @@ const TopJurors: React.FC = () => { Top Jurors - +
{!isUndefined(topJurors) ? topJurors.map((juror) => ) : [...Array(5)].map((_, i) => )}