diff --git a/web/src/app.tsx b/web/src/app.tsx
index f6a705ac6..594489272 100644
--- a/web/src/app.tsx
+++ b/web/src/app.tsx
@@ -6,6 +6,7 @@ import "react-toastify/dist/ReactToastify.css";
import Web3Provider from "context/Web3Provider";
import QueryClientProvider from "context/QueryClientProvider";
import StyledComponentsProvider from "context/StyledComponentsProvider";
+import { FilterProvider } from "context/FilterProvider";
import RefetchOnBlock from "context/RefetchOnBlock";
import Layout from "layout/index";
import Home from "./pages/Home";
@@ -20,16 +21,18 @@ const App: React.FC = () => {
-
- }>
- } />
- } />
- } />
- } />
- } />
- Justice not found here ¯\_( ͡° ͜ʖ ͡°)_/¯} />
-
-
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ Justice not found here ¯\_( ͡° ͜ʖ ͡°)_/¯} />
+
+
+
diff --git a/web/src/assets/svgs/icons/grid.svg b/web/src/assets/svgs/icons/grid.svg
new file mode 100644
index 000000000..eb3fa4e05
--- /dev/null
+++ b/web/src/assets/svgs/icons/grid.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/src/assets/svgs/icons/list.svg b/web/src/assets/svgs/icons/list.svg
new file mode 100644
index 000000000..767198a5d
--- /dev/null
+++ b/web/src/assets/svgs/icons/list.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/src/components/CasesDisplay/CasesGrid.tsx b/web/src/components/CasesDisplay/CasesGrid.tsx
index b9e9c9415..7f465ff1e 100644
--- a/web/src/components/CasesDisplay/CasesGrid.tsx
+++ b/web/src/components/CasesDisplay/CasesGrid.tsx
@@ -1,12 +1,27 @@
import React from "react";
-import styled from "styled-components";
+import styled, { css } from "styled-components";
import { StandardPagination } from "@kleros/ui-components-library";
+import { smallScreenStyle } from "styles/smallScreenStyle";
+import { useFiltersContext } from "context/FilterProvider";
import { CasesPageQuery } from "queries/useCasesQuery";
import DisputeCard from "components/DisputeCard";
+import CasesListHeader from "./CasesListHeader";
-const Container = styled.div`
+const GridContainer = styled.div`
+ display: grid;
+ gap: 16px;
+ grid-template-columns: repeat(3, minmax(50px, 1fr));
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ ${smallScreenStyle(css`
+ display: flex;
+ flex-wrap: wrap;
+ `)}
+`;
+const ListContainer = styled.div`
display: flex;
- flex-wrap: wrap;
+ flex-direction: column;
justify-content: center;
gap: 8px;
`;
@@ -26,13 +41,23 @@ export interface ICasesGrid {
}
const CasesGrid: React.FC = ({ disputes, currentPage, setCurrentPage, numberDisputes, casesPerPage }) => {
+ const { isList } = useFiltersContext();
return (
<>
-
- {disputes.map((dispute, i) => {
- return ;
- })}
-
+ {!isList ? (
+
+ {disputes.map((dispute, i) => {
+ return ;
+ })}
+
+ ) : (
+
+ {isList && }
+ {disputes.map((dispute, i) => {
+ return ;
+ })}
+
+ )}
theme.secondaryText} !important;
+ }
+
+ ${smallScreenStyle(css`
+ display: none;
+ `)}
+`;
+
+const tooltipMsg =
+ "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 CasesListHeader: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+ Category
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CasesListHeader;
diff --git a/web/src/components/CasesDisplay/Filters.tsx b/web/src/components/CasesDisplay/Filters.tsx
index d74c06574..a4b596cd7 100644
--- a/web/src/components/CasesDisplay/Filters.tsx
+++ b/web/src/components/CasesDisplay/Filters.tsx
@@ -1,6 +1,9 @@
import React from "react";
import styled, { useTheme } from "styled-components";
import { DropdownSelect } from "@kleros/ui-components-library";
+import { useFiltersContext } from "context/FilterProvider";
+import ListIcon from "svgs/icons/list.svg";
+import GridIcon from "svgs/icons/grid.svg";
const Container = styled.div`
display: flex;
@@ -9,8 +12,33 @@ const Container = styled.div`
width: fit-content;
`;
+const StyledGridIcon = styled(GridIcon)<{ isList: boolean }>`
+ cursor: pointer;
+ transition: fill 0.2s ease;
+ fill: ${({ theme, isList }) => (isList ? theme.secondaryText : theme.primaryBlue)};
+ width: 16px;
+ height: 16px;
+`;
+
+const IconsContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+`;
+
+const StyledListIcon = styled(ListIcon)<{ isList: boolean }>`
+ cursor: pointer;
+ transition: fill 0.2s ease;
+ fill: ${({ theme, isList }) => (isList ? theme.primaryBlue : theme.secondaryText)};
+ width: 16px;
+ height: 17px;
+`;
+
const Filters: React.FC = () => {
const theme = useTheme();
+ const { isList, setIsList } = useFiltersContext();
+
return (
{
defaultValue={0}
callback={() => {}}
/>
+
+ setIsList(false)} />
+ setIsList(true)} />
+
);
};
diff --git a/web/src/components/DisputeCard/DisputeInfo.tsx b/web/src/components/DisputeCard/DisputeInfo.tsx
index 0b3568097..6ea8f9940 100644
--- a/web/src/components/DisputeCard/DisputeInfo.tsx
+++ b/web/src/components/DisputeCard/DisputeInfo.tsx
@@ -7,10 +7,14 @@ import LawBalanceIcon from "svgs/icons/law-balance.svg";
import PileCoinsIcon from "svgs/icons/pile-coins.svg";
import Field from "../Field";
-const Container = styled.div`
+const Container = styled.div<{ isCard: boolean }>`
display: flex;
- flex-direction: column;
- gap: 8px;
+ flex-direction: ${({ isCard }) => (isCard ? "column" : "row")};
+ gap: ${({ isCard }) => (isCard ? "8px" : "48px")};
+ justify-content: ${({ isCard }) => (isCard ? "center" : "space-between")};
+ align-items: center;
+ width: 100%;
+ height: 100%;
`;
const getPeriodPhrase = (period: Periods): string => {
@@ -33,16 +37,31 @@ export interface IDisputeInfo {
rewards?: string;
period?: Periods;
date?: number;
+ isCard?: boolean;
}
-const DisputeInfo: React.FC = ({ courtId, court, category, rewards, period, date }) => {
+const formatDate = (date: number) => {
+ const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
+ const startingDate = new Date(date * 1000);
+ const formattedDate = startingDate.toLocaleDateString("en-US", options);
+ return formattedDate;
+};
+
+const DisputeInfo: React.FC = ({ courtId, court, category, rewards, period, date, isCard = true }) => {
return (
-
- {category && }
- {court && courtId && }
- {rewards && }
+
+ {court && courtId && (
+
+ )}
+ {category && }
+ {rewards && }
{typeof period !== "undefined" && date && (
-
+
)}
);
diff --git a/web/src/components/DisputeCard/PeriodBanner.tsx b/web/src/components/DisputeCard/PeriodBanner.tsx
index fb8dd3e0a..0ad1ac647 100644
--- a/web/src/components/DisputeCard/PeriodBanner.tsx
+++ b/web/src/components/DisputeCard/PeriodBanner.tsx
@@ -3,7 +3,7 @@ import styled, { Theme } from "styled-components";
import { Periods } from "consts/periods";
const Container = styled.div>`
- height: 45px;
+ height: ${({ isCard }) => (isCard ? "45px" : "100%")};
width: auto;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
@@ -21,11 +21,11 @@ const Container = styled.div>`
margin-right: 8px;
}
}
- ${({ theme, period }) => {
+ ${({ theme, period, isCard }) => {
const [frontColor, backgroundColor] = getPeriodColors(period, theme);
return `
- border-top: 5px solid ${frontColor};
- background-color: ${backgroundColor};
+ ${isCard ? `border-top: 5px solid ${frontColor}` : `border-left: 5px solid ${frontColor}`};
+ ${isCard && `background-color: ${backgroundColor}`};
.front-color {
color: ${frontColor};
}
@@ -41,6 +41,7 @@ const Container = styled.div>`
export interface IPeriodBanner {
id: number;
period: Periods;
+ isCard?: boolean;
}
const getPeriodColors = (period: Periods, theme: Theme): [string, string] => {
@@ -65,9 +66,9 @@ const getPeriodLabel = (period: Periods): string => {
}
};
-const PeriodBanner: React.FC = ({ id, period }) => (
-
-
+const PeriodBanner: React.FC = ({ id, period, isCard = true }) => (
+
+ {isCard && }
);
diff --git a/web/src/components/DisputeCard/index.tsx b/web/src/components/DisputeCard/index.tsx
index 2579d0130..8ecc73562 100644
--- a/web/src/components/DisputeCard/index.tsx
+++ b/web/src/components/DisputeCard/index.tsx
@@ -5,6 +5,7 @@ import { formatEther } from "viem";
import { StyledSkeleton } from "components/StyledSkeleton";
import { Card } from "@kleros/ui-components-library";
import { Periods } from "consts/periods";
+import { useFiltersContext } from "context/FilterProvider";
import { CasesPageQuery } from "queries/useCasesQuery";
import { useCourtPolicy } from "queries/useCourtPolicy";
import { useDisputeTemplate } from "queries/useDisputeTemplate";
@@ -18,8 +19,13 @@ const StyledCard = styled(Card)`
width: auto;
height: 260px;
`;
+const StyledListItem = styled(Card)`
+ display: flex;
+ width: 100%;
+ height: 64px;
+`;
-const Container = styled.div`
+const CardContainer = styled.div`
height: 215px;
padding: 24px;
display: flex;
@@ -29,6 +35,25 @@ const Container = styled.div`
margin: 0;
}
`;
+const ListContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 32px;
+ width: 100%;
+ margin-right: 2%;
+ h3 {
+ margin: 0;
+ }
+`;
+
+const ListTitle = styled.div`
+ display: flex;
+ height: 100%;
+ justify-content: start;
+ align-items: center;
+ min-width: 40vw;
+`;
export const getPeriodEndTimestamp = (
lastPeriodChange: string,
@@ -46,6 +71,7 @@ const DisputeCard: React.FC = ({
lastPeriodChange,
court,
}) => {
+ const { isList } = useFiltersContext();
const currentPeriodIndex = Periods[period];
const rewards = `≥ ${formatEther(court.feeForJuror)} ETH`;
const date =
@@ -63,18 +89,38 @@ const DisputeCard: React.FC = ({
const category = disputeTemplate ? disputeTemplate.category : undefined;
const navigate = useNavigate();
return (
- navigate(`/cases/${id.toString()}`)}>
-
-
- {title}
-
-
-
+ <>
+ {!isList ? (
+ navigate(`/cases/${id.toString()}`)}>
+
+
+ {title}
+
+
+
+ ) : (
+ navigate(`/cases/${id.toString()}`)}>
+
+
+
+ {title}
+
+
+
+
+ )}
+ >
);
};
diff --git a/web/src/components/Field.tsx b/web/src/components/Field.tsx
index af7b09df8..5c5b92540 100644
--- a/web/src/components/Field.tsx
+++ b/web/src/components/Field.tsx
@@ -7,9 +7,10 @@ const FieldContainer = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
+ white-space: nowrap;
.value {
flex-grow: 1;
- text-align: end;
+ text-align: ${({ isCard }) => (isCard ? "end" : "center")};
color: ${({ theme }) => theme.primaryText};
}
svg {
@@ -27,6 +28,7 @@ const FieldContainer = styled.div`
type FieldContainerProps = {
width?: string;
+ isCard?: boolean;
};
interface IField {
@@ -35,12 +37,17 @@ interface IField {
value: string;
link?: string;
width?: string;
+ isCard?: boolean;
}
-const Field: React.FC = ({ icon: Icon, name, value, link, width }) => (
-
- {}
-
+const Field: React.FC = ({ icon: Icon, name, value, link, width, isCard = true }) => (
+
+ {isCard && (
+ <>
+
+
+ >
+ )}
{link ? (
{value}
diff --git a/web/src/context/FilterProvider.tsx b/web/src/context/FilterProvider.tsx
new file mode 100644
index 000000000..267059c5e
--- /dev/null
+++ b/web/src/context/FilterProvider.tsx
@@ -0,0 +1,25 @@
+import React, { useState, createContext, useContext } from "react";
+
+interface IFilters {
+ isList: boolean;
+ setIsList: (arg0: boolean) => void;
+}
+
+const Context = createContext({
+ isList: false,
+ setIsList: () => {
+ //
+ },
+});
+
+export const FilterProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
+ const [isList, setIsList] = useState(false);
+
+ const value = {
+ isList,
+ setIsList,
+ };
+ return {children};
+};
+
+export const useFiltersContext = () => useContext(Context);
diff --git a/web/src/context/Web3Provider.tsx b/web/src/context/Web3Provider.tsx
index 1677ebbac..c6e161578 100644
--- a/web/src/context/Web3Provider.tsx
+++ b/web/src/context/Web3Provider.tsx
@@ -8,7 +8,7 @@ import { jsonRpcProvider } from "wagmi/providers/jsonRpc";
import { useToggleTheme } from "hooks/useToggleThemeContext";
import { useTheme } from "styled-components";
-const chains = [mainnet, arbitrumGoerli, gnosisChiado];
+const chains = [arbitrumGoerli, gnosisChiado];
const projectId = process.env.WALLETCONNECT_PROJECT_ID ?? "6efaa26765fa742153baf9281e218217";
const { publicClient, webSocketPublicClient } = configureChains(chains, [
diff --git a/web/src/hooks/queries/useCasesQuery.ts b/web/src/hooks/queries/useCasesQuery.ts
index c6b813924..6d1cf7a9e 100644
--- a/web/src/hooks/queries/useCasesQuery.ts
+++ b/web/src/hooks/queries/useCasesQuery.ts
@@ -5,8 +5,8 @@ import { graphqlQueryFnHelper } from "~src/utils/graphqlQueryFnHelper";
export type { CasesPageQuery };
const casesQuery = graphql(`
- query CasesPage($skip: Int) {
- disputes(first: 3, skip: $skip, orderBy: lastPeriodChange, orderDirection: desc) {
+ query CasesPage($first: Int, $skip: Int) {
+ disputes(first: $first, skip: $skip, orderBy: lastPeriodChange, orderDirection: desc) {
id
arbitrated {
id
@@ -26,12 +26,12 @@ const casesQuery = graphql(`
}
`);
-export const useCasesQuery = (skip: number) => {
+export const useCasesQuery = (skip: number, first = 3) => {
const isEnabled = skip !== undefined;
return useQuery({
- queryKey: [`useCasesQuery${skip}`],
+ queryKey: [`useCasesQuery${skip},${first}`],
enabled: isEnabled,
- queryFn: async () => await graphqlQueryFnHelper(casesQuery, { skip: skip }),
+ queryFn: async () => await graphqlQueryFnHelper(casesQuery, { skip, first }),
});
};
diff --git a/web/src/hooks/useWindowWidth.ts b/web/src/hooks/useWindowWidth.ts
new file mode 100644
index 000000000..ff74a14ba
--- /dev/null
+++ b/web/src/hooks/useWindowWidth.ts
@@ -0,0 +1,19 @@
+import { useState, useEffect } from "react";
+
+export const useWindowWidth = () => {
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setWindowWidth(window.innerWidth);
+ };
+
+ window.addEventListener("resize", handleResize);
+
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ };
+ }, []);
+
+ return windowWidth;
+};
diff --git a/web/src/pages/Cases/index.tsx b/web/src/pages/Cases/index.tsx
index 86b623fdb..a34ce9a04 100644
--- a/web/src/pages/Cases/index.tsx
+++ b/web/src/pages/Cases/index.tsx
@@ -1,10 +1,12 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { Routes, Route } from "react-router-dom";
import { useCasesQuery } from "queries/useCasesQuery";
+import { useWindowWidth } from "hooks/useWindowWidth";
+import { BREAKPOINT_TABLET_SCREEN } from "styles/tabletScreenStyle";
import CasesDisplay from "components/CasesDisplay";
import CaseDetails from "./CaseDetails";
-
+import { useFiltersContext } from "context/FilterProvider";
const Container = styled.div`
width: 100%;
min-height: calc(100vh - 144px);
@@ -14,8 +16,18 @@ const Container = styled.div`
const Cases: React.FC = () => {
const [currentPage, setCurrentPage] = useState(1);
- const casesPerPage = 3;
- const { data } = useCasesQuery(casesPerPage * (currentPage - 1));
+ const windowWidth = useWindowWidth();
+ const { isList, setIsList } = useFiltersContext();
+ const screenIsBig = windowWidth > BREAKPOINT_TABLET_SCREEN;
+ const casesPerPage = screenIsBig ? 9 : 3;
+ const { data } = useCasesQuery(casesPerPage * (currentPage - 1), casesPerPage);
+
+ useEffect(() => {
+ if (!screenIsBig && isList) {
+ setIsList(false);
+ }
+ }, [screenIsBig]);
+
return (
diff --git a/web/src/styles/tabletScreenStyle.ts b/web/src/styles/tabletScreenStyle.ts
new file mode 100644
index 000000000..7b1fe25e7
--- /dev/null
+++ b/web/src/styles/tabletScreenStyle.ts
@@ -0,0 +1,9 @@
+import { css, DefaultTheme, FlattenInterpolation, ThemeProps } from "styled-components";
+
+export const BREAKPOINT_TABLET_SCREEN = 1024;
+
+export const tabletScreenStyle = (styleFn: () => FlattenInterpolation>) => css`
+ @media (max-width: ${BREAKPOINT_TABLET_SCREEN}px) {
+ ${() => styleFn()}
+ }
+`;