Skip to content

Commit 4849f30

Browse files
authored
Merge pull request #1794 from kleros/feat/case-bookmark
feat(web): starred-cases
2 parents fb69a01 + 1e74faa commit 4849f30

File tree

6 files changed

+182
-6
lines changed

6 files changed

+182
-6
lines changed

web/src/assets/svgs/icons/star.svg

+1
Loading

web/src/components/CaseStarButton.tsx

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { useMemo } from "react";
2+
import styled, { css } from "styled-components";
3+
4+
import { Button, Tooltip } from "@kleros/ui-components-library";
5+
6+
import Star from "svgs/icons/star.svg";
7+
8+
import useIsDesktop from "hooks/useIsDesktop";
9+
import useStarredCases from "hooks/useStarredCases";
10+
11+
const StyledButton = styled(Button)<{ starred: boolean }>`
12+
background: none;
13+
padding: 0 0 2px 0;
14+
15+
.button-svg {
16+
width: 24px;
17+
height: 24px;
18+
margin: 0;
19+
fill: none;
20+
21+
path {
22+
stroke: ${({ theme }) => theme.secondaryPurple};
23+
}
24+
${({ starred }) =>
25+
starred &&
26+
css`
27+
fill: ${({ theme }) => theme.secondaryPurple};
28+
`};
29+
}
30+
31+
:hover {
32+
background: none;
33+
}
34+
`;
35+
36+
const CaseStarButton: React.FC<{ id: string }> = ({ id }) => {
37+
const { starredCases, starCase } = useStarredCases();
38+
const isDesktop = useIsDesktop();
39+
const starred = useMemo(() => Boolean(starredCases.has(id)), [id, starredCases]);
40+
const text = starred ? "Remove from favorite" : "Add to favorite";
41+
return (
42+
<Tooltip {...{ text }} place={isDesktop ? "right" : "bottom"}>
43+
<StyledButton
44+
Icon={Star}
45+
text=""
46+
starred={starred}
47+
aria-label={text}
48+
aria-checked={starred}
49+
onClick={(e) => {
50+
e.stopPropagation();
51+
starCase(id);
52+
}}
53+
/>
54+
</Tooltip>
55+
);
56+
};
57+
58+
export default CaseStarButton;

web/src/components/FavoriteCases.tsx

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
4+
import { StandardPagination } from "@kleros/ui-components-library";
5+
6+
import useStarredCases from "hooks/useStarredCases";
7+
import { isUndefined } from "utils/index";
8+
9+
import { DisputeDetailsFragment, useCasesQuery } from "queries/useCasesQuery";
10+
11+
import { responsiveSize } from "styles/responsiveSize";
12+
13+
import DisputeView from "components/DisputeView";
14+
import { SkeletonDisputeCard } from "components/StyledSkeleton";
15+
16+
const Container = styled.div`
17+
margin-top: ${responsiveSize(48, 80)};
18+
`;
19+
20+
const Title = styled.h1`
21+
margin-bottom: 4px;
22+
`;
23+
24+
const DisputeContainer = styled.div`
25+
--gap: 16px;
26+
display: grid;
27+
grid-template-columns: repeat(auto-fill, minmax(min(100%, max(312px, (100% - var(--gap) * 2)/3)), 1fr));
28+
align-items: stretch;
29+
gap: var(--gap);
30+
`;
31+
32+
const StyledLabel = styled.label`
33+
display: block;
34+
color: ${({ theme }) => theme.primaryBlue};
35+
cursor: pointer;
36+
margin-bottom: ${responsiveSize(12, 16)};
37+
:hover {
38+
color: ${({ theme }) => theme.secondaryBlue};
39+
}
40+
`;
41+
42+
const StyledPagination = styled(StandardPagination)`
43+
margin-top: 24px;
44+
margin-left: auto;
45+
margin-right: auto;
46+
`;
47+
48+
const FavoriteCases: React.FC = () => {
49+
const { starredCaseIds, clearAll } = useStarredCases();
50+
51+
const [currentPage, setCurrentPage] = useState(1);
52+
const casesPerPage = 3;
53+
const totalPages = Math.ceil(starredCaseIds.length / casesPerPage);
54+
55+
const { data } = useCasesQuery((currentPage - 1) * casesPerPage, casesPerPage, {
56+
id_in: starredCaseIds,
57+
});
58+
59+
const disputes: DisputeDetailsFragment[] = useMemo(() => data?.disputes as DisputeDetailsFragment[], [data]);
60+
61+
return starredCaseIds.length > 0 && (isUndefined(disputes) || disputes.length > 0) ? (
62+
<Container>
63+
<Title>Favorite Cases</Title>
64+
<StyledLabel onClick={clearAll}>Clear all</StyledLabel>
65+
<DisputeContainer>
66+
{isUndefined(disputes)
67+
? Array.from({ length: 3 }).map((_, index) => <SkeletonDisputeCard key={index} />)
68+
: disputes.map((dispute) => <DisputeView key={dispute.id} {...dispute} overrideIsList />)}
69+
</DisputeContainer>
70+
{totalPages > 1 ? (
71+
<StyledPagination
72+
currentPage={currentPage}
73+
numPages={totalPages}
74+
callback={(page: number) => setCurrentPage(page)}
75+
/>
76+
) : null}
77+
</Container>
78+
) : null;
79+
};
80+
81+
export default FavoriteCases;

web/src/hooks/useStarredCases.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useMemo } from "react";
2+
3+
import { useLocalStorage } from "./useLocalStorage";
4+
5+
const useStarredCases = () => {
6+
const initialValue = new Set<string>();
7+
8+
const [localStarredCases, setLocalStarredCases] = useLocalStorage("starredCases", Array.from(initialValue));
9+
10+
const starredCases = useMemo(() => new Set<string>(localStarredCases), [localStarredCases]);
11+
const starredCaseIds = Array.from(starredCases.keys());
12+
13+
const starCase = (id: string) => {
14+
if (starredCases.has(id)) starredCases.delete(id);
15+
else starredCases.add(id);
16+
17+
setLocalStarredCases(Array.from(starredCases));
18+
};
19+
20+
const clearAll = () => {
21+
setLocalStarredCases(Array.from(initialValue));
22+
};
23+
return { starredCases, starredCaseIds, starCase, clearAll };
24+
};
25+
26+
export default useStarredCases;

web/src/pages/Cases/CaseDetails/index.tsx

+11-3
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
1212

1313
import { responsiveSize } from "styles/responsiveSize";
1414

15+
import CaseStarButton from "components/CaseStarButton";
16+
import ScrollTop from "components/ScrollTop";
17+
1518
import Appeal from "./Appeal";
1619
import Evidence from "./Evidence";
1720
import MaintenanceButtons from "./MaintenanceButtons";
1821
import Overview from "./Overview";
1922
import Tabs from "./Tabs";
2023
import Timeline from "./Timeline";
2124
import Voting from "./Voting";
22-
import ScrollTop from "components/ScrollTop";
2325

2426
const Container = styled.div``;
2527

@@ -38,8 +40,11 @@ const HeaderContainer = styled.div`
3840
`;
3941

4042
const Header = styled.h1`
41-
margin: 0;
43+
display: flex;
44+
align-items: center;
4245
flex: 1;
46+
gap: 8px;
47+
margin: 0;
4348
`;
4449

4550
const CaseDetails: React.FC = () => {
@@ -53,7 +58,10 @@ const CaseDetails: React.FC = () => {
5358
<VotingContextProvider>
5459
<Container>
5560
<HeaderContainer>
56-
<Header>Case #{id}</Header>
61+
<Header>
62+
Case #{id} {id ? <CaseStarButton id={id} /> : null}
63+
</Header>
64+
5765
<MaintenanceButtons />
5866
</HeaderContainer>
5967
<Timeline {...{ currentPeriodIndex, dispute }} />

web/src/pages/Dashboard/index.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import React, { useMemo } from "react";
22
import styled from "styled-components";
33

4-
import { MAX_WIDTH_LANDSCAPE } from "styles/landscapeStyle";
5-
import { responsiveSize } from "styles/responsiveSize";
6-
74
import { useNavigate, useParams } from "react-router-dom";
85
import { useAccount } from "wagmi";
96

@@ -15,8 +12,12 @@ import { useUserQuery } from "queries/useUser";
1512

1613
import { OrderDirection } from "src/graphql/graphql";
1714

15+
import { MAX_WIDTH_LANDSCAPE } from "styles/landscapeStyle";
16+
import { responsiveSize } from "styles/responsiveSize";
17+
1818
import CasesDisplay from "components/CasesDisplay";
1919
import ConnectWallet from "components/ConnectWallet";
20+
import FavoriteCases from "components/FavoriteCases";
2021
import ScrollTop from "components/ScrollTop";
2122

2223
import Courts from "./Courts";
@@ -94,6 +95,7 @@ const Dashboard: React.FC = () => {
9495
<ConnectWallet />
9596
</ConnectWalletContainer>
9697
)}
98+
<FavoriteCases />
9799
<ScrollTop />
98100
</Container>
99101
);

0 commit comments

Comments
 (0)