diff --git a/web/src/assets/svgs/icons/star.svg b/web/src/assets/svgs/icons/star.svg new file mode 100644 index 000000000..055e853c0 --- /dev/null +++ b/web/src/assets/svgs/icons/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/CaseStarButton.tsx b/web/src/components/CaseStarButton.tsx new file mode 100644 index 000000000..1ba097686 --- /dev/null +++ b/web/src/components/CaseStarButton.tsx @@ -0,0 +1,58 @@ +import React, { useMemo } from "react"; +import styled, { css } from "styled-components"; + +import { Button, Tooltip } from "@kleros/ui-components-library"; + +import Star from "svgs/icons/star.svg"; + +import useIsDesktop from "hooks/useIsDesktop"; +import useStarredCases from "hooks/useStarredCases"; + +const StyledButton = styled(Button)<{ starred: boolean }>` + background: none; + padding: 0 0 2px 0; + + .button-svg { + width: 24px; + height: 24px; + margin: 0; + fill: none; + + path { + stroke: ${({ theme }) => theme.secondaryPurple}; + } + ${({ starred }) => + starred && + css` + fill: ${({ theme }) => theme.secondaryPurple}; + `}; + } + + :hover { + background: none; + } +`; + +const CaseStarButton: React.FC<{ id: string }> = ({ id }) => { + const { starredCases, starCase } = useStarredCases(); + const isDesktop = useIsDesktop(); + const starred = useMemo(() => Boolean(starredCases.has(id)), [id, starredCases]); + const text = starred ? "Remove from favorite" : "Add to favorite"; + return ( + + { + e.stopPropagation(); + starCase(id); + }} + /> + + ); +}; + +export default CaseStarButton; diff --git a/web/src/components/FavoriteCases.tsx b/web/src/components/FavoriteCases.tsx new file mode 100644 index 000000000..c90ab4f5b --- /dev/null +++ b/web/src/components/FavoriteCases.tsx @@ -0,0 +1,81 @@ +import React, { useMemo, useState } from "react"; +import styled from "styled-components"; + +import { StandardPagination } from "@kleros/ui-components-library"; + +import useStarredCases from "hooks/useStarredCases"; +import { isUndefined } from "utils/index"; + +import { DisputeDetailsFragment, useCasesQuery } from "queries/useCasesQuery"; + +import { responsiveSize } from "styles/responsiveSize"; + +import DisputeView from "components/DisputeView"; +import { SkeletonDisputeCard } from "components/StyledSkeleton"; + +const Container = styled.div` + margin-top: ${responsiveSize(48, 80)}; +`; + +const Title = styled.h1` + margin-bottom: 4px; +`; + +const DisputeContainer = styled.div` + --gap: 16px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, max(312px, (100% - var(--gap) * 2)/3)), 1fr)); + align-items: stretch; + gap: var(--gap); +`; + +const StyledLabel = styled.label` + display: block; + color: ${({ theme }) => theme.primaryBlue}; + cursor: pointer; + margin-bottom: ${responsiveSize(12, 16)}; + :hover { + color: ${({ theme }) => theme.secondaryBlue}; + } +`; + +const StyledPagination = styled(StandardPagination)` + margin-top: 24px; + margin-left: auto; + margin-right: auto; +`; + +const FavoriteCases: React.FC = () => { + const { starredCaseIds, clearAll } = useStarredCases(); + + const [currentPage, setCurrentPage] = useState(1); + const casesPerPage = 3; + const totalPages = Math.ceil(starredCaseIds.length / casesPerPage); + + const { data } = useCasesQuery((currentPage - 1) * casesPerPage, casesPerPage, { + id_in: starredCaseIds, + }); + + const disputes: DisputeDetailsFragment[] = useMemo(() => data?.disputes as DisputeDetailsFragment[], [data]); + + return starredCaseIds.length > 0 && (isUndefined(disputes) || disputes.length > 0) ? ( + + Favorite Cases + Clear all + + {isUndefined(disputes) + ? Array.from({ length: 3 }).map((_, index) => ) + : disputes.map((dispute) => )} + + {totalPages > 1 ? ( + setCurrentPage(page)} + /> + ) : null} + + ) : null; +}; + +export default FavoriteCases; diff --git a/web/src/hooks/useStarredCases.tsx b/web/src/hooks/useStarredCases.tsx new file mode 100644 index 000000000..eb6109306 --- /dev/null +++ b/web/src/hooks/useStarredCases.tsx @@ -0,0 +1,26 @@ +import { useMemo } from "react"; + +import { useLocalStorage } from "./useLocalStorage"; + +const useStarredCases = () => { + const initialValue = new Set(); + + const [localStarredCases, setLocalStarredCases] = useLocalStorage("starredCases", Array.from(initialValue)); + + const starredCases = useMemo(() => new Set(localStarredCases), [localStarredCases]); + const starredCaseIds = Array.from(starredCases.keys()); + + const starCase = (id: string) => { + if (starredCases.has(id)) starredCases.delete(id); + else starredCases.add(id); + + setLocalStarredCases(Array.from(starredCases)); + }; + + const clearAll = () => { + setLocalStarredCases(Array.from(initialValue)); + }; + return { starredCases, starredCaseIds, starCase, clearAll }; +}; + +export default useStarredCases; diff --git a/web/src/pages/Cases/CaseDetails/index.tsx b/web/src/pages/Cases/CaseDetails/index.tsx index 28c97a060..64cc12424 100644 --- a/web/src/pages/Cases/CaseDetails/index.tsx +++ b/web/src/pages/Cases/CaseDetails/index.tsx @@ -12,6 +12,9 @@ import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; import { responsiveSize } from "styles/responsiveSize"; +import CaseStarButton from "components/CaseStarButton"; +import ScrollTop from "components/ScrollTop"; + import Appeal from "./Appeal"; import Evidence from "./Evidence"; import MaintenanceButtons from "./MaintenanceButtons"; @@ -19,7 +22,6 @@ import Overview from "./Overview"; import Tabs from "./Tabs"; import Timeline from "./Timeline"; import Voting from "./Voting"; -import ScrollTop from "components/ScrollTop"; const Container = styled.div``; @@ -38,8 +40,11 @@ const HeaderContainer = styled.div` `; const Header = styled.h1` - margin: 0; + display: flex; + align-items: center; flex: 1; + gap: 8px; + margin: 0; `; const CaseDetails: React.FC = () => { @@ -53,7 +58,10 @@ const CaseDetails: React.FC = () => { -
Case #{id}
+
+ Case #{id} {id ? : null} +
+
diff --git a/web/src/pages/Dashboard/index.tsx b/web/src/pages/Dashboard/index.tsx index 37f06b6e6..e8a890e66 100644 --- a/web/src/pages/Dashboard/index.tsx +++ b/web/src/pages/Dashboard/index.tsx @@ -1,9 +1,6 @@ import React, { useMemo } from "react"; import styled from "styled-components"; -import { MAX_WIDTH_LANDSCAPE } from "styles/landscapeStyle"; -import { responsiveSize } from "styles/responsiveSize"; - import { useNavigate, useParams } from "react-router-dom"; import { useAccount } from "wagmi"; @@ -15,8 +12,12 @@ import { useUserQuery } from "queries/useUser"; import { OrderDirection } from "src/graphql/graphql"; +import { MAX_WIDTH_LANDSCAPE } 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 Courts from "./Courts"; @@ -94,6 +95,7 @@ const Dashboard: React.FC = () => { )} +
);