diff --git a/.vscode/settings.json b/.vscode/settings.json index ce255610..c27e6da8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "svg.preview.background": "black", "cSpell.words": [ "offchain" - ] + ], + "editor.autoClosingBrackets": "always" } diff --git a/src/modules/explorer/components/FilterNFTDialog.tsx b/src/modules/explorer/components/FilterNFTDialog.tsx new file mode 100644 index 00000000..c2cdbc18 --- /dev/null +++ b/src/modules/explorer/components/FilterNFTDialog.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from "react" +import { Grid, TextField, Typography, styled, withStyles } from "@material-ui/core" +import { ResponsiveDialog } from "./ResponsiveDialog" +import { SmallButton } from "modules/common/SmallButton" +import { TokensFilters } from "../pages/NFTs" + +interface Props { + currentFilters: TokensFilters | undefined + open: boolean + handleClose: () => void + saveFilters: (options: TokensFilters) => void +} + +const SectionTitle = styled(Typography)({ + fontSize: "18px !important", + fontWeight: 600 +}) + +const Container = styled(Grid)(({ theme }) => ({ + marginTop: 6, + gap: 24, + [theme.breakpoints.down("sm")]: { + marginTop: 30 + } +})) + +const CustomTextField = withStyles({ + root: { + "& .MuiInput-root": { + fontWeight: 300, + textAlign: "initial" + }, + "& .MuiInputBase-input": { + textAlign: "initial", + background: "#2F3438", + borderRadius: 8, + padding: 16 + }, + "& p": { + position: "absolute", + right: 16, + fontWeight: 300 + }, + "& .MuiInputBase-root": { + textWeight: 300 + }, + "& .MuiInput-underline": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:after": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none !important" + } + } +})(TextField) + +export const FilterNFTDialog: React.FC = ({ open, handleClose, saveFilters, currentFilters }) => { + const [owner, setOwner] = useState("") + const [valueMin, setValueMin] = useState() + const [valueMax, setValueMax] = useState() + + const ariaLabel = { "aria-label": "description" } + + useEffect(() => { + if (currentFilters) { + setOwner(currentFilters?.owner) + setValueMin(currentFilters.valueMin) + setValueMax(currentFilters.valueMax) + } + }, [currentFilters]) + + const showFilters = () => { + const filterObject: TokensFilters = { + owner: owner, + valueMin: valueMin, + valueMax: valueMax + } + saveFilters(filterObject) + handleClose() + } + + return ( + <> + + + + Value + + + + setValueMin(event.target.value)} + name="test" + value={valueMin} + placeholder="Min" + inputProps={ariaLabel} + type="number" + /> + + + setValueMax(event.target.value)} + name="test" + value={valueMax} + placeholder="Max" + type="number" + inputProps={ariaLabel} + /> + + + + + Creator Address + + + setOwner(event.target.value)} + style={{ width: "100%" }} + name="test" + value={owner} + placeholder="Address" + inputProps={ariaLabel} + /> + + + + + + Apply + + + + + ) +} diff --git a/src/modules/explorer/components/FiltersDialog.tsx b/src/modules/explorer/components/FiltersDialog.tsx index 270f5042..f626cb5d 100644 --- a/src/modules/explorer/components/FiltersDialog.tsx +++ b/src/modules/explorer/components/FiltersDialog.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react" import { ResponsiveDialog } from "./ResponsiveDialog" import { Grid, styled } from "@material-ui/core" import { Typography } from "@mui/material" -import { Dropdown } from "./Dropdown" import { ProposalStatus } from "services/services/dao/mappers/proposal/types" import { SmallButton } from "modules/common/SmallButton" import { Order, ProposalType } from "./FiltersUserDialog" diff --git a/src/modules/explorer/components/FiltersTokensDialog.tsx b/src/modules/explorer/components/FiltersTokensDialog.tsx new file mode 100644 index 00000000..18d04c8e --- /dev/null +++ b/src/modules/explorer/components/FiltersTokensDialog.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from "react" +import { Grid, TextField, Typography, styled, withStyles } from "@material-ui/core" +import { ResponsiveDialog } from "./ResponsiveDialog" +import { SmallButton } from "modules/common/SmallButton" +import { TokensFilters } from "../pages/Treasury" + +interface Props { + currentFilters: TokensFilters | undefined + open: boolean + handleClose: () => void + saveFilters: (options: TokensFilters) => void +} + +const SectionTitle = styled(Typography)({ + fontSize: "18px !important", + fontWeight: 600 +}) + +const Container = styled(Grid)(({ theme }) => ({ + marginTop: 6, + gap: 24, + [theme.breakpoints.down("sm")]: { + marginTop: 30 + } +})) + +const CustomTextField = withStyles({ + root: { + "& .MuiInput-root": { + fontWeight: 300, + textAlign: "initial" + }, + "& .MuiInputBase-input": { + textAlign: "initial", + background: "#2F3438", + borderRadius: 8, + padding: 16 + }, + "& .MuiInputBase-root": { + textWeight: 300 + }, + "& .MuiInput-underline": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:after": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none !important" + } + } +})(TextField) + +export const FilterTokenDialog: React.FC = ({ open, handleClose, saveFilters, currentFilters }) => { + const [token, setToken] = useState("") + const [balanceMin, setBalanceMin] = useState() + const [balanceMax, setBalanceMax] = useState() + + const ariaLabel = { "aria-label": "description" } + + useEffect(() => { + if (currentFilters) { + setToken(currentFilters?.token) + setBalanceMin(currentFilters.balanceMin) + setBalanceMax(currentFilters.balanceMax) + } + }, [currentFilters]) + + const showFilters = () => { + const filterObject: TokensFilters = { + token: token, + balanceMin: balanceMin, + balanceMax: balanceMax + } + saveFilters(filterObject) + handleClose() + } + + return ( + <> + + + + Token + + + setToken(event.target.value)} + style={{ width: "40%" }} + name="test" + value={token} + placeholder="Token" + inputProps={ariaLabel} + /> + + + + Balance + + + + setBalanceMin(event.target.value)} + name="test" + value={balanceMin} + placeholder="Min" + inputProps={ariaLabel} + type="number" + /> + + + setBalanceMax(event.target.value)} + name="test" + value={balanceMax} + placeholder="Max" + type="number" + inputProps={ariaLabel} + /> + + + + + + + Apply + + + + + ) +} diff --git a/src/modules/explorer/components/FiltersTransactionsDialog.tsx b/src/modules/explorer/components/FiltersTransactionsDialog.tsx new file mode 100644 index 00000000..a4c28876 --- /dev/null +++ b/src/modules/explorer/components/FiltersTransactionsDialog.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useState } from "react" +import { Grid, TextField, Typography, styled, withStyles } from "@material-ui/core" +import { ResponsiveDialog } from "./ResponsiveDialog" +import { SmallButton } from "modules/common/SmallButton" +import { TransactionsFilters } from "../pages/Treasury" + +export enum TransactionStatus { + COMPLETED = "applied", + PENDING = "pending", + FAILED = "failed" +} + +interface Props { + currentFilters: TransactionsFilters | undefined + open: boolean + handleClose: () => void + saveFilters: (options: TransactionsFilters) => void +} + +const SectionTitle = styled(Typography)({ + fontSize: "18px !important", + fontWeight: 600 +}) + +const Container = styled(Grid)(({ theme }) => ({ + marginTop: 6, + gap: 24, + [theme.breakpoints.down("sm")]: { + marginTop: 30 + } +})) + +const CustomTextField = withStyles({ + root: { + "& .MuiInput-root": { + fontWeight: 300, + textAlign: "initial" + }, + "& .MuiInputBase-input": { + textAlign: "initial", + background: "#2F3438", + borderRadius: 8, + padding: 16 + }, + "& .MuiInputBase-root": { + textWeight: 300 + }, + "& .MuiInput-underline": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:before": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:after": { + borderBottom: "none !important" + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none !important" + } + } +})(TextField) + +const StatusButton = styled(Grid)(({ theme }) => ({ + "background": theme.palette.primary.main, + "padding": "8px 16px", + "borderRadius": 50, + "marginRight": 16, + "marginBottom": 16, + "cursor": "pointer", + "textTransform": "capitalize", + "&:hover": { + background: "rgba(129, 254, 183, .4)" + } +})) + +interface StatusOption { + label: string +} + +export const FilterTransactionsDialog: React.FC = ({ open, handleClose, saveFilters, currentFilters }) => { + const [status, setStatus] = useState([]) + const [token, setToken] = useState("") + const [sender, setSender] = useState("") + const [receiver, setReceiver] = useState("") + + const [filters, setFilters] = useState() + const ariaLabel = { "aria-label": "description" } + + useEffect(() => { + setStatus([]) + setStatusOptions() + if (currentFilters) { + setToken(currentFilters?.token) + setSender(currentFilters.sender) + setReceiver(currentFilters.receiver) + setFilters(currentFilters.status) + } + }, [currentFilters]) + + const setStatusOptions = () => { + const values = Object.values(TransactionStatus) + for (const item in values) { + const obj = { + label: values[item] + } + setStatus(oldArray => [...oldArray, obj]) + } + } + + const isSelected = (item: StatusOption) => { + return filters && filters.label === item.label ? true : false + } + + const saveStatus = (status: StatusOption) => { + if (status.label === filters?.label) { + setFilters(undefined) + } else { + setFilters(status) + } + } + + const showFilters = () => { + const filterObject: TransactionsFilters = { + token: token, + receiver: receiver, + sender: sender, + status: filters + } + saveFilters(filterObject) + handleClose() + } + + return ( + <> + + + + Sort by + + + + {status.length > 0 && + status.map((item, index) => { + return ( + + saveStatus(item)}>{item.label} + + ) + })} + + + + Token + + + setToken(event.target.value)} + style={{ width: "40%" }} + name="test" + value={token} + placeholder="Token" + inputProps={ariaLabel} + /> + + + + Receiving Address + + + setReceiver(event.target.value)} + name="test" + value={receiver} + placeholder="Address" + inputProps={ariaLabel} + /> + + + + Sending Address + + + setSender(event.target.value)} + name="test" + value={sender} + placeholder="Address" + inputProps={ariaLabel} + /> + + + + + + Apply + + + + + ) +} diff --git a/src/modules/explorer/components/ResponsiveDialog.tsx b/src/modules/explorer/components/ResponsiveDialog.tsx index d0522987..7527a265 100644 --- a/src/modules/explorer/components/ResponsiveDialog.tsx +++ b/src/modules/explorer/components/ResponsiveDialog.tsx @@ -51,7 +51,7 @@ export const ResponsiveDialog: React.FC<{ return isSmall ? ( - + {onGoBack !== undefined ? ( diff --git a/src/modules/explorer/pages/NFTs/index.tsx b/src/modules/explorer/pages/NFTs/index.tsx index b27385f8..c4a13dfd 100644 --- a/src/modules/explorer/pages/NFTs/index.tsx +++ b/src/modules/explorer/pages/NFTs/index.tsx @@ -6,7 +6,7 @@ import { NFTDialog } from "modules/explorer/components/NFTDialog" import { ProposalFormContainer, ProposalFormDefaultValues } from "modules/explorer/components/ProposalForm" import { UserBadge } from "modules/explorer/components/UserBadge" -import React, { useState } from "react" +import React, { useMemo, useState } from "react" import { NFTDAOHolding } from "services/bakingBad/tokenBalances" import { useTezos } from "services/beacon/hooks/useTezos" import { useDAONFTHoldings } from "services/contracts/baseDAO/hooks/useDAOHoldings" @@ -17,7 +17,11 @@ import { useIsProposalButtonDisabled } from "../../../../services/contracts/base import { SmallButton } from "../../../common/SmallButton" import { parseUnits } from "services/contracts/utils" import ReactPaginate from "react-paginate" +import FilterAltIcon from "@mui/icons-material/FilterAlt" + import "../DAOList/styles.css" +import { SearchInput } from "../DAOList/components/Searchbar" +import { FilterNFTDialog } from "modules/explorer/components/FilterNFTDialog" const Card = styled(ContentContainer)(({ theme }) => ({ boxSizing: "border-box", @@ -46,11 +50,16 @@ const NFTTitle = styled(Typography)({ fontWeight: 500 }) -const ProposalsFooter = styled(Grid)({ - padding: "16px 46px", - minHeight: 34 +const FiltersContainer = styled(Grid)({ + cursor: "pointer" }) +export interface TokensFilters { + owner: string | null + valueMin: string | undefined + valueMax: string | undefined +} + export const NFTs: React.FC = () => { const theme = useTheme() const daoId = useDAOID() @@ -61,6 +70,9 @@ export const NFTs: React.FC = () => { const [defaultValues, setDefaultValues] = useState() const [selectedNFT, setSelectedNFT] = useState() const isMobileSmall = useMediaQuery(theme.breakpoints.down("sm")) + const [openFiltersDialog, setOpenFiltersDialog] = useState(false) + const [searchText, setSearchText] = useState("") + const [filters, setFilters] = useState() const onClickNFT = (nft: NFTDAOHolding) => { setSelectedNFT(nft) @@ -94,7 +106,7 @@ export const NFTs: React.FC = () => { const onCloseTransfer = () => { setOpenTransfer(false) } - const value = isMobileSmall ? 6 : 3 + const value = isMobileSmall ? 6 : 4 const shouldDisable = useIsProposalButtonDisabled(daoId) const [currentPage, setCurrentPage] = useState(0) @@ -105,17 +117,80 @@ export const NFTs: React.FC = () => { const newOffset = (event.selected * value) % nftHoldings.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } + const filterByName = (text: string) => { + setSearchText(text.trim()) + } + + const handleFilters = (filters: TokensFilters) => { + setFilters(filters) + } + + const handleCloseFiltersModal = () => { + setOpenFiltersDialog(false) + } + + const rows = useMemo(() => { + const handleFilterData = (holdings: NFTDAOHolding[]) => { + let data = holdings.slice() + if (filters?.owner && filters.owner !== "") { + data = holdings.filter(trx => + trx.token?.firstCreator ? trx.token?.firstCreator.toLowerCase() === filters.owner?.toLocaleLowerCase() : null + ) + } + if (filters?.valueMin && filters.valueMin !== "") { + data = holdings.filter(trx => trx.balance.isGreaterThanOrEqualTo(filters.valueMin!)) + } + if (filters?.valueMax && filters.valueMax !== "") { + data = holdings.filter(trx => trx.balance.isLessThanOrEqualTo(filters.valueMax!)) + } + return data + } + + if (!nftHoldings) { + return [] + } + let holdings = nftHoldings.slice() + + if (filters) { + holdings = handleFilterData(holdings) + } + + if (searchText) { + holdings = holdings.filter( + holding => holding.token && holding.token.name.toLowerCase().includes(searchText.toLowerCase()) + ) + } + return holdings + }, [nftHoldings, searchText, filters]) + const pageCount = Math.ceil(nftHoldings ? nftHoldings.length / value : 0) return ( <> - + + + setOpenFiltersDialog(true)} + xs={isMobileSmall ? 6 : 2} + item + container + direction="row" + alignItems="center" + > + + Filter & Sort + + + + + - {!nftHoldings ? ( + {!rows ? ( <> @@ -123,7 +198,7 @@ export const NFTs: React.FC = () => { ) : ( <> - {nftHoldings.slice(offset, offset + value).map((nft, i) => ( + {rows.slice(offset, offset + value).map((nft, i) => ( { ))} - {!(nftHoldings && nftHoldings.length > 0) ? ( - + {!(rows && rows.length > 0) ? ( + - + No items - + ) : null} { defaultValues={defaultValues} defaultTab={1} /> + ) } diff --git a/src/modules/explorer/pages/Proposals/index.tsx b/src/modules/explorer/pages/Proposals/index.tsx index c6a9045e..e88818da 100644 --- a/src/modules/explorer/pages/Proposals/index.tsx +++ b/src/modules/explorer/pages/Proposals/index.tsx @@ -261,7 +261,7 @@ export const Proposals: React.FC = () => { direction="row" alignItems="center" > - + Filter & Sort diff --git a/src/modules/explorer/pages/Treasury/components/BalancesTable.tsx b/src/modules/explorer/pages/Treasury/components/BalancesTable.tsx index d81dd3f1..2d65325e 100644 --- a/src/modules/explorer/pages/Treasury/components/BalancesTable.tsx +++ b/src/modules/explorer/pages/Treasury/components/BalancesTable.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react" +import React, { useEffect, useMemo, useState } from "react" import { Card, CardContent, Grid, styled, Typography, useMediaQuery, useTheme } from "@material-ui/core" import { ProposalFormContainer, ProposalFormDefaultValues } from "modules/explorer/components/ProposalForm" import { DAOHolding } from "services/bakingBad/tokenBalances" @@ -12,6 +12,14 @@ import { toShortAddress } from "services/contracts/utils" import { CopyButton } from "modules/common/CopyButton" import ReactPaginate from "react-paginate" import "../../DAOList/styles.css" +import FilterAltIcon from "@mui/icons-material/FilterAlt" +import { SearchInput } from "../../DAOList/components/Searchbar" +import { TokensFilters } from ".." +import { FilterTokenDialog } from "modules/explorer/components/FiltersTokensDialog" + +const FiltersContainer = styled(Grid)({ + cursor: "pointer" +}) const TokenSymbol = styled(Typography)(({ theme }) => ({ color: theme.palette.secondary.main, @@ -20,17 +28,6 @@ const TokenSymbol = styled(Typography)(({ theme }) => ({ fontSize: 24 })) -const MobileTableHeader = styled(Grid)({ - width: "100%", - padding: 20, - borderBottom: "0.3px solid #3D3D3D" -}) - -const MobileTableRow = styled(Grid)({ - padding: "30px", - borderBottom: "0.3px solid #3D3D3D" -}) - interface RowData { symbol: string address: string @@ -78,17 +75,6 @@ const createData = (daoHolding: DAOHolding): RowData => { const titles = ["Token Balances", "Address", "Balance"] as const -const titleDataMatcher = (title: (typeof titles)[number], rowData: RowData) => { - switch (title) { - case "Token Balances": - return rowData.symbol - case "Address": - return rowData.address - case "Balance": - return rowData.amount - } -} - interface TableProps { rows: RowData[] tezosBalance: BigNumber @@ -100,7 +86,6 @@ interface TableProps { const BalancesList: React.FC = ({ rows, - tezosBalance, openTokenTransferModal, openXTZTransferModal, shouldDisable, @@ -108,85 +93,82 @@ const BalancesList: React.FC = ({ }) => { const [currentPage, setCurrentPage] = useState(0) const [offset, setOffset] = useState(0) - const value = isMobileSmall ? 6 : 2 - // Invoke when user click to request another page. + const value = isMobileSmall ? 6 : 5 + const [list, setList] = useState(rows) + + useEffect(() => { + setList(rows) + }, [rows]) + const handlePageClick = (event: { selected: number }) => { if (rows) { const newOffset = (event.selected * value) % rows.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } const pageCount = Math.ceil(rows ? rows.length / value : 0) return ( - - - - XTZ - - - Balance - - {tezosBalance.toString()} - - openXTZTransferModal()} - disabled={shouldDisable} - > - Transfer - + {list && list.length > 0 ? ( + <> + {list.slice(offset, offset + value).map((row, i) => ( + + + + {row.symbol} + + {toShortAddress(row.address)} + + + + Balance + + {row.amount} + + + row.symbol === "XTZ" ? openXTZTransferModal() : openTokenTransferModal(row.address) + } + disabled={shouldDisable} + > + Transfer + + + + - - - - - {rows.slice(offset, offset + value).map((row, i) => ( - - - - {row.symbol} - - {toShortAddress(row.address)} - - - - Balance - - {row.amount} - - openTokenTransferModal(row.address)} - disabled={shouldDisable} - > - Transfer - - - - - - ))} - - - + ))} + + + + + ) : ( + No items + )} ) } @@ -200,6 +182,13 @@ export const BalancesTable: React.FC = () => { const { data: tezosBalance } = useTezosBalance(daoId) const [openTransfer, setOpenTransfer] = useState(false) const [defaultValues, setDefaultValues] = useState() + const [openFiltersDialog, setOpenFiltersDialog] = useState(false) + const [searchText, setSearchText] = useState("") + const [filters, setFilters] = useState() + + const filterByName = (text: string) => { + setSearchText(text.trim()) + } const onCloseTransfer = () => { setOpenTransfer(false) @@ -245,17 +234,81 @@ export const BalancesTable: React.FC = () => { setOpenTransfer(true) } + const handleFilters = (filters: TokensFilters) => { + setFilters(filters) + } + + const handleCloseFiltersModal = () => { + setOpenFiltersDialog(false) + } + const rows = useMemo(() => { + const handleFilterData = (holdings: DAOHolding[]) => { + let data = holdings.slice() + if (filters?.token && filters.token !== "") { + data = holdings.filter(trx => trx.token?.symbol.toLowerCase() === filters.token?.toLocaleLowerCase()) + } + if (filters?.balanceMin && filters.balanceMin !== "") { + data = holdings.filter(trx => trx.balance.isGreaterThanOrEqualTo(filters.balanceMin!)) + } + if (filters?.balanceMax && filters.balanceMax !== "") { + data = holdings.filter(trx => trx.balance.isLessThanOrEqualTo(filters.balanceMax!)) + } + return data + } + if (!tokenHoldings) { return [] } + let holdings = tokenHoldings.slice() + const xtz: DAOHolding = { + token: { + symbol: "XTZ", + id: "XTZ", + contract: "", + token_id: 0, + name: "", + decimals: 6, + network: "mainnet", + supply: tezosBalance || new BigNumber(0), + standard: "" + }, + balance: tezosBalance || new BigNumber(0) + } + holdings.unshift(xtz) + + if (filters) { + holdings = handleFilterData(holdings) + } - return tokenHoldings.map(createData) - }, [tokenHoldings]) + if (searchText) { + holdings = holdings.filter( + holding => holding.token && holding.token.name.toLowerCase().includes(searchText.toLowerCase()) + ) + } + + return holdings.map(createData) + }, [tokenHoldings, searchText, filters, tezosBalance]) return ( <> + + setOpenFiltersDialog(true)} + xs={isSmall ? 6 : 2} + item + container + direction="row" + alignItems="center" + > + + Filter & Sort + + + + + { defaultValues={defaultValues} defaultTab={0} /> + ) } diff --git a/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx b/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx index 6b598071..61b81cef 100644 --- a/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx +++ b/src/modules/explorer/pages/Treasury/components/TransfersTable.tsx @@ -1,5 +1,5 @@ +import React, { useEffect, useMemo, useState } from "react" import dayjs from "dayjs" -import React, { useMemo, useState } from "react" import { Grid, Link, @@ -20,9 +20,10 @@ import { ContentContainer } from "modules/explorer/components/ContentContainer" import { networkNameMap } from "services/bakingBad" import { toShortAddress } from "services/contracts/utils" import { ReactComponent as BulletIcon } from "assets/img/bullet.svg" -import { CopyButton } from "modules/common/CopyButton" import ReactPaginate from "react-paginate" import "../../DAOList/styles.css" +import OpenInNewIcon from "@mui/icons-material/OpenInNew" +import numbro from "numbro" const localizedFormat = require("dayjs/plugin/localizedFormat") dayjs.extend(localizedFormat) @@ -33,7 +34,8 @@ const createData = (transfer: TransferWithBN) => { date: dayjs(transfer.date).format("L"), address: transfer.recipient, amount: transfer.amount.dp(10, 1).toString(), - hash: transfer.hash + hash: transfer.hash, + type: transfer.type } } @@ -65,16 +67,70 @@ const ProposalsFooterMobile = styled(Grid)({ borderBottomRightRadius: 8 }) +const ItemContainer = styled(Grid)(({ theme }) => ({ + padding: "40px 48px", + gap: 8, + borderRadius: 8, + background: theme.palette.primary.main, + [theme.breakpoints.down("sm")]: { + padding: "30px 38px", + gap: 20 + } +})) + +const Container = styled(Grid)({ + gap: 24, + display: "grid" +}) + +const Title = styled(Typography)({ + color: "#fff", + fontSize: 24 +}) + +const Subtitle = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.light, + fontSize: 16, + fontWeight: 300 +})) + +const AmountText = styled(Typography)(({ theme }) => ({ + color: "#fff", + fontSize: 18, + fontWeight: 300, + lineHeight: "160%" +})) + +const BlockExplorer = styled(Typography)({ + "fontSize": 16, + "fontWeight": 400, + "cursor": "pointer", + "display": "flex", + "alignItems": "center", + "& svg": { + fontSize: 16, + marginRight: 6 + } +}) + interface RowData { token: string date: string amount: string address: string hash: string + type: string | undefined } const Titles = ["Token", "Date", "Recipient", "Amount"] +const formatConfig = { + average: true, + mantissa: 1, + thousandSeparated: true, + trimMantissa: true +} + const titleDataMatcher = (title: (typeof Titles)[number], rowData: RowData) => { switch (title) { case "Token": @@ -122,6 +178,7 @@ const MobileTransfersTable: React.FC<{ data: RowData[]; network: Network }> = ({ const newOffset = (event.selected * 5) % data.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } const pageCount = Math.ceil(data ? data.length / 5 : 0) @@ -183,84 +240,93 @@ const MobileTransfersTable: React.FC<{ data: RowData[]; network: Network }> = ({ ) } -const DesktopTransfersTable: React.FC<{ data: RowData[]; network: Network }> = ({ data: rows, network }) => { +const TransfersTableItems: React.FC<{ data: RowData[]; network: Network }> = ({ data: rows, network }) => { const [currentPage, setCurrentPage] = useState(0) const [offset, setOffset] = useState(0) + const theme = useTheme() + const isSmall = useMediaQuery(theme.breakpoints.down("xs")) + + useEffect(() => { + setOffset(0) + }, [rows]) + + const openBlockExplorer = (hash: string) => { + window.open(`https://${networkNameMap[network]}.tzkt.io/` + hash, "_blank") + } + // Invoke when user click to request another page. const handlePageClick = (event: { selected: number }) => { if (rows) { const newOffset = (event.selected * 5) % rows.length setOffset(newOffset) setCurrentPage(event.selected) + window.scrollTo({ top: 0, behavior: "smooth" }) } } const pageCount = Math.ceil(rows ? rows.length / 5 : 0) return ( <> - - - - {Titles.map((title, i) => ( - {title} - ))} - - - - {rows.slice(offset, offset + 5).map((row, i) => ( - - -
- {row.token} -
-
- {row.date} - - {toShortAddress(row.address)} - - - {row.amount} -
- ))} - {!(rows && rows.length > 0) ? ( - - - - No items - - - - ) : null} -
-
- - - + {rows && rows.length > 0 ? ( + <> + + {rows.slice(offset, offset + 5).map((row, i) => { + return ( + + + + + {row.token} + + + To {toShortAddress(row.address)} + {isSmall ? null : } + {dayjs(row.date).format("ll")} + + + + + + {row.type ? (row.type === "Deposit" ? "-" : "+") : null}{" "} + {isSmall ? numbro(row.amount).format(formatConfig) : row.amount} {row.token}{" "} + + + + openBlockExplorer(row.hash)}> + + View on Block Explorer + + + + + ) + })} + + + + + + + ) : ( + No items + )} ) } export const TransfersTable: React.FC<{ transfers: TransferWithBN[] }> = ({ transfers }) => { - const theme = useTheme() - const isSmall = useMediaQuery(theme.breakpoints.down("sm")) - const rows = useMemo(() => { if (!transfers) { return [] @@ -273,10 +339,10 @@ export const TransfersTable: React.FC<{ transfers: TransferWithBN[] }> = ({ tran return ( - {isSmall ? ( - + {rows && rows.length > 0 ? ( + ) : ( - + No items )} ) diff --git a/src/modules/explorer/pages/Treasury/index.tsx b/src/modules/explorer/pages/Treasury/index.tsx index b9a92a6c..55a92428 100644 --- a/src/modules/explorer/pages/Treasury/index.tsx +++ b/src/modules/explorer/pages/Treasury/index.tsx @@ -1,12 +1,10 @@ -import { Button, Grid, Theme, Tooltip, Typography, useMediaQuery, useTheme } from "@material-ui/core" -import { ProposalFormContainer } from "modules/explorer/components/ProposalForm" - import React, { useMemo, useState } from "react" +import { Button, Grid, Theme, Tooltip, Typography, useMediaQuery, useTheme } from "@material-ui/core" import { useDAO } from "services/services/dao/hooks/useDAO" import { useDAOID } from "../DAO/router" import { BalancesTable } from "./components/BalancesTable" import { TransfersTable } from "./components/TransfersTable" -import { useTransfers } from "../../../../services/contracts/baseDAO/hooks/useTransfers" +import { TransferWithBN, useTransfers } from "../../../../services/contracts/baseDAO/hooks/useTransfers" import { InfoIcon } from "../../components/styled/InfoIcon" import { useIsProposalButtonDisabled } from "../../../../services/contracts/baseDAO/hooks/useCycleInfo" import { styled } from "@material-ui/core" @@ -19,6 +17,14 @@ import { SmallButton } from "modules/common/SmallButton" import { ContentContainer } from "modules/explorer/components/ContentContainer" import { CopyButton } from "modules/common/CopyButton" import { TreasuryDialog } from "./components/TreasuryDialog" +import { SearchInput } from "../DAOList/components/Searchbar" +import FilterAltIcon from "@mui/icons-material/FilterAlt" +import { FilterTransactionsDialog } from "modules/explorer/components/FiltersTransactionsDialog" +import { StatusOption } from "modules/explorer/components/FiltersUserDialog" + +const FiltersContainer = styled(Grid)({ + cursor: "pointer" +}) const ItemGrid = styled(Grid)({ width: "inherit" @@ -50,6 +56,19 @@ const TitleText = styled(Typography)({ } }) +export interface TransactionsFilters { + token: string | null + sender: string | null + receiver: string | null + status: StatusOption | undefined +} + +export interface TokensFilters { + token: string | null + balanceMin: string | undefined + balanceMax: string | undefined +} + const StyledTab = styled(Button)(({ theme, isSelected }: { theme: Theme; isSelected: boolean }) => ({ "fontSize": 18, "height": 40, @@ -89,6 +108,9 @@ export const Treasury: React.FC = () => { const { data: dao } = useDAO(daoId) const [openTransfer, setOpenTransfer] = useState(false) const [selectedTab, setSelectedTab] = React.useState(0) + const [searchText, setSearchText] = useState("") + const [filters, setFilters] = useState() + const [openFiltersDialog, setOpenFiltersDialog] = useState(false) const { data: transfers } = useTransfers(daoId) @@ -101,6 +123,54 @@ export const Treasury: React.FC = () => { setOpenTransfer(false) } + const currentTransfers = useMemo(() => { + const handleFilterData = (allTransfers: TransferWithBN[]) => { + let data = allTransfers.slice() + if (filters?.receiver && filters.receiver !== "") { + data = allTransfers.filter(trx => trx.recipient === filters.receiver) + } + if (filters?.sender && filters.sender !== "") { + data = allTransfers.filter(trx => trx.sender === filters.sender) + } + if (filters?.token && filters.token !== "") { + data = allTransfers.filter(trx => trx.token?.symbol.toLocaleLowerCase() === filters.token?.toLocaleLowerCase()) + } + if (filters?.status && filters.status.label !== "") { + data = allTransfers.filter(trx => trx.status === filters.status?.label) + } + return data + } + + if (transfers) { + let allTransfers = transfers.slice() + if (filters) { + allTransfers = handleFilterData(allTransfers) + } + + if (searchText) { + return allTransfers.filter( + formattedDao => formattedDao.name && formattedDao.name.toLowerCase().includes(searchText.toLowerCase()) + ) + } + + return allTransfers + } + + return [] + }, [searchText, transfers, filters]) + + const filterByName = (filter: string) => { + setSearchText(filter.trim()) + } + + const handleFilters = (filters: TransactionsFilters) => { + setFilters(filters) + } + + const handleCloseFiltersModal = () => { + setOpenFiltersDialog(false) + } + return ( <> @@ -131,7 +201,7 @@ export const Treasury: React.FC = () => { justifyContent="flex-end" alignItems="center" style={{ gap: 15 }} - direction={isMobileSmall ? "column" : "row"} + direction={isMobileSmall ? "row" : "row"} xs={isMobileSmall ? undefined : true} > {dao && ( @@ -139,7 +209,7 @@ export const Treasury: React.FC = () => { item xs container - justifyContent="flex-end" + justifyContent={"flex-end"} direction="row" style={isMobileSmall ? {} : { marginLeft: 30 }} > @@ -175,7 +245,9 @@ export const Treasury: React.FC = () => { } + startIcon={ + + } variant="contained" disableElevation={true} onClick={() => handleChangeTab(0)} @@ -186,7 +258,9 @@ export const Treasury: React.FC = () => { } + startIcon={ + + } disableElevation={true} variant="contained" onClick={() => handleChangeTab(1)} @@ -197,13 +271,15 @@ export const Treasury: React.FC = () => { } + startIcon={ + + } disableElevation={true} variant="contained" onClick={() => handleChangeTab(2)} isSelected={selectedTab === 2} > - History + Transactions @@ -219,7 +295,23 @@ export const Treasury: React.FC = () => { - + + setOpenFiltersDialog(true)} + xs={isMobileSmall ? 6 : 2} + item + container + direction="row" + alignItems="center" + > + + Filter & Sort + + + + + + @@ -230,6 +322,12 @@ export const Treasury: React.FC = () => { open={openTransfer} handleClose={onCloseTransfer} /> + ) } diff --git a/src/services/contracts/baseDAO/hooks/useTransfers.ts b/src/services/contracts/baseDAO/hooks/useTransfers.ts index 8bfc54b8..5e5fe81a 100644 --- a/src/services/contracts/baseDAO/hooks/useTransfers.ts +++ b/src/services/contracts/baseDAO/hooks/useTransfers.ts @@ -25,6 +25,7 @@ export interface TransferWithBN { hash: string type?: "Withdrawal" | "Deposit" token?: BNToken + status?: string } export const useTransfers = (contractAddress: string) => { diff --git a/src/theme/index.ts b/src/theme/index.ts index 164e773e..54f56b2a 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -213,7 +213,8 @@ export const theme = createTheme({ borderBottom: "none" }, "&:before": { - borderBottom: "none" + borderBottom: "none", + transition: "none" }, "&:hover:not($disabled):not($focused):not($error):before": { borderBottom: "none"