diff --git a/explorer/src/components/Back/index.tsx b/explorer/src/components/Back/index.tsx index b58349f2..e9b4c68f 100644 --- a/explorer/src/components/Back/index.tsx +++ b/explorer/src/components/Back/index.tsx @@ -1,21 +1,26 @@ import { ChevronLeft } from "lucide-react"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { EMPTY_STRING } from "@/constants"; -export interface BackProps { +interface BackProps { className?: string; } export const Back: React.FC = ({ className }) => { const navigate = useNavigate(); + const { state, pathname } = useLocation(); + const backPath = pathname.split("/").slice(0, -1); + const { title, handler } = state?.from + ? { handler: () => navigate(-1), title: "Back" } + : { handler: () => navigate(backPath.join("/")), title: `Back to ${backPath.slice(-1)}` }; return ( ); }; diff --git a/explorer/src/components/DataTable/index.tsx b/explorer/src/components/DataTable/index.tsx index 4b9e05b5..1d6ae94e 100644 --- a/explorer/src/components/DataTable/index.tsx +++ b/explorer/src/components/DataTable/index.tsx @@ -21,7 +21,7 @@ export function DataTable({ columns, data }: DataTableProps {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} @@ -35,10 +35,7 @@ export function DataTable({ columns, data }: DataTableProps ( {row.getVisibleCells().map((cell) => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} diff --git a/explorer/src/components/HelperIndicator/index.tsx b/explorer/src/components/HelperIndicator/index.tsx index cf0c6b60..8e7c38bb 100644 --- a/explorer/src/components/HelperIndicator/index.tsx +++ b/explorer/src/components/HelperIndicator/index.tsx @@ -8,6 +8,8 @@ const getIndicatorColorClass = (type: Page): string => { return "bg-indicator-blue"; case "portal": return "bg-indicator-green"; + case "module": + return "bg-indicator-orange"; default: return "bg-transparent"; } diff --git a/explorer/src/components/Link/index.tsx b/explorer/src/components/Link/index.tsx index fde83294..7fc0ac64 100644 --- a/explorer/src/components/Link/index.tsx +++ b/explorer/src/components/Link/index.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from "react"; -import { LinkProps, Link as RouterLink } from "react-router-dom"; +import { LinkProps, Link as RouterLink, useLocation } from "react-router-dom"; import { useNetworkContext } from "@/providers/network-provider/context"; import { CHAIN_ID_ROUTE } from "@/routes/constants"; @@ -8,8 +8,9 @@ export const Link: React.FC = ({ children, ...pro const { network: { network }, } = useNetworkContext(); + const { pathname } = useLocation(); return ( - + {children} ); diff --git a/explorer/src/components/NavLink/index.tsx b/explorer/src/components/NavLink/index.tsx index 57be56d7..e9e0f16d 100644 --- a/explorer/src/components/NavLink/index.tsx +++ b/explorer/src/components/NavLink/index.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from "react"; -import { NavLinkProps, NavLink as RouterNavLink } from "react-router-dom"; +import { NavLinkProps, NavLink as RouterNavLink, useLocation } from "react-router-dom"; import { useNetworkContext } from "@/providers/network-provider/context"; import { CHAIN_ID_ROUTE } from "@/routes/constants"; @@ -8,8 +8,9 @@ export const NavLink: React.FC = ({ children, const { network: { network }, } = useNetworkContext(); + const { pathname } = useLocation(); return ( - + {children} ); diff --git a/explorer/src/components/NotFoundPage/index.tsx b/explorer/src/components/NotFoundPage/index.tsx index 8eb3f970..905467ad 100644 --- a/explorer/src/components/NotFoundPage/index.tsx +++ b/explorer/src/components/NotFoundPage/index.tsx @@ -28,7 +28,7 @@ export const NotFoundPage: React.FC = ({ id, page }) => { className="flex gap-2 border border-solid rounded-md px-4 py-3 border-button-secondary-border hover:border-button-secondary-hover" > - Go Back + {`Go Back to ${page}s`} diff --git a/explorer/src/components/Pagination/index.tsx b/explorer/src/components/Pagination/index.tsx index e5e5a8dc..5ce02dfe 100644 --- a/explorer/src/components/Pagination/index.tsx +++ b/explorer/src/components/Pagination/index.tsx @@ -1,36 +1,34 @@ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { useSearchParams } from "react-router-dom"; -import { CURRENT_PAGE_DEFAULT, ITEMS_PER_PAGE_DEFAULT, ZERO } from "@/constants"; +import { ITEMS_PER_PAGE_DEFAULT } from "@/constants"; import { EQueryParams } from "@/enums/queryParams"; import { displayAmountWithComma } from "@/utils/amountUtils"; +import { pageBySearchParams } from "@/utils/paginationUtils"; import { IPaginationProps } from "./interface"; -export const Pagination = ({ itemsCount, handleSkip }: IPaginationProps) => { +export const Pagination = ({ itemsCount, handlePage }: IPaginationProps) => { const [searchParams, setSearchParams] = useSearchParams(); - const page = searchParams.get(EQueryParams.PAGE); + const currentPage = pageBySearchParams(searchParams, itemsCount); + + useEffect(() => { + handlePage(currentPage); + }, [currentPage, handlePage, searchParams]); - const [currentPage, setCurrentPage] = useState(Number(page) || 1); const totalPages = Math.ceil(itemsCount / ITEMS_PER_PAGE_DEFAULT); const disablePrev = currentPage === 1; const disableNext = currentPage === totalPages; - useEffect(() => { - const currentSearchParams = new URLSearchParams(searchParams); - currentSearchParams.set(EQueryParams.PAGE, currentPage.toString()); - setSearchParams(currentSearchParams); - }, [currentPage, searchParams, setSearchParams]); - const inputRef = useRef(null); const handlePageChange = (newPage: number) => { if (newPage >= 1 && newPage <= totalPages && inputRef && inputRef.current) { - setCurrentPage(newPage); - handleSkip(newPage === CURRENT_PAGE_DEFAULT ? ZERO : (newPage - 1) * ITEMS_PER_PAGE_DEFAULT); inputRef.current.value = newPage.toString(); + searchParams.set(EQueryParams.PAGE, newPage.toString()); + setSearchParams(searchParams); } }; diff --git a/explorer/src/components/Pagination/interface.ts b/explorer/src/components/Pagination/interface.ts index 50518a84..867a07e8 100644 --- a/explorer/src/components/Pagination/interface.ts +++ b/explorer/src/components/Pagination/interface.ts @@ -1,4 +1,4 @@ export interface IPaginationProps { itemsCount: number; - handleSkip: React.Dispatch>; + handlePage: (page: number) => void; } diff --git a/explorer/src/components/ui/table.tsx b/explorer/src/components/ui/table.tsx index 78920eb8..a17a4db8 100644 --- a/explorer/src/components/ui/table.tsx +++ b/explorer/src/components/ui/table.tsx @@ -37,7 +37,7 @@ const TableRow = React.forwardRef[] => [ + { + accessorKey: "id", + header: () => "ID", + cell: ({ row }) => { + const id = row.getValue("id") as string; + return ( + + {cropString(id)} + + ); + }, + }, + { + accessorKey: "name", + header: () => ( +
+ + Module Name +
+ ), + cell: ({ row }) => { + const name = row.getValue("name") as string; + return name; + }, + }, + { + accessorKey: "description", + header: () => "Module Description", + cell: ({ row }) => { + const description = row.getValue("description"); + return

{description as string}

; + }, + }, + { + accessorKey: "moduleAddress", + header: () =>

Contract Address

, + cell: ({ row }) => { + const address = row.getValue("moduleAddress") as string; + return ( + + {cropString(address)} + + ); + }, + }, +]; diff --git a/explorer/src/index.css b/explorer/src/index.css index 375b8ba4..8b7c971f 100644 --- a/explorer/src/index.css +++ b/explorer/src/index.css @@ -15,6 +15,7 @@ --indicator-blue: #2D4EC3; --indicator-magenta: #D6247A; --indicator-green: #41AC00; + --indicator-orange: #B93800; --text-primary: #0D0D12; --text-secondary: #3D3D51; @@ -98,5 +99,34 @@ stroke: #9096B2; } + .table-row-transition { + transition: all cubic-bezier(0, 0.52, 1, 1) .7s; + + td:last-child { + display: flex; + justify-content: flex-end; + position: relative; + transition: transform 0.5s; + + &:after { + content: '\203A'; + position: absolute; + opacity: 0; + top: 15px; + right: 0; + transition: 0.5s; + scale: 2; + } + } + } + + .table-row-transition:hover { + td:last-child { + transform: translateX(-20px); + &:after { + opacity: 1; + } + } + } } \ No newline at end of file diff --git a/explorer/src/interfaces/components/index.ts b/explorer/src/interfaces/components/index.ts index 52612c7e..244a369d 100644 --- a/explorer/src/interfaces/components/index.ts +++ b/explorer/src/interfaces/components/index.ts @@ -4,4 +4,4 @@ export interface NavigationProps { submenu?: JSX.Element; } -export type Page = "schema" | "portal" | "attestation"; +export type Page = "schema" | "portal" | "attestation" | "module"; diff --git a/explorer/src/interfaces/swr/enum.ts b/explorer/src/interfaces/swr/enum.ts index 8a4f4194..7580491a 100644 --- a/explorer/src/interfaces/swr/enum.ts +++ b/explorer/src/interfaces/swr/enum.ts @@ -5,4 +5,6 @@ export enum SWRKeys { GET_ATTESTATION_LIST = "getAttestationList", GET_ATTESTATION_COUNT = "getAttestationCount", GET_RECENT_ATTESTATION = "getRecentAttestations", + GET_MODULE_LIST = "getModuleList", + GET_MODULE_COUNT = "getModuleCount", } diff --git a/explorer/src/pages/Attestations/index.tsx b/explorer/src/pages/Attestations/index.tsx index ae36e7ec..0ce51a02 100644 --- a/explorer/src/pages/Attestations/index.tsx +++ b/explorer/src/pages/Attestations/index.tsx @@ -1,6 +1,5 @@ import { OrderDirection } from "@verax-attestation-registry/verax-sdk/lib/types/.graphclient"; import { useState } from "react"; -import { useSearchParams } from "react-router-dom"; import useSWR from "swr"; import { DataTable } from "@/components/DataTable"; @@ -10,6 +9,7 @@ import { columns } from "@/constants/columns/attestation"; import { EQueryParams } from "@/enums/queryParams"; import { SWRKeys } from "@/interfaces/swr/enum"; import { useNetworkContext } from "@/providers/network-provider/context"; +import { getItemsByPage, pageBySearchParams } from "@/utils/paginationUtils"; import { ListSwitcher } from "./components/ListSwitcher"; @@ -19,8 +19,16 @@ export const Attestations: React.FC = () => { network: { chain }, } = useNetworkContext(); - const [searchParams] = useSearchParams(); - const [skip, setSkip] = useState(ZERO); + const { data: attestationsCount } = useSWR( + `${SWRKeys.GET_ATTESTATION_COUNT}/${chain.id}`, + () => sdk.attestation.getAttestationIdCounter() as Promise, + ); + + const totalItems = attestationsCount ?? ZERO; + const searchParams = new URLSearchParams(window.location.search); + const page = pageBySearchParams(searchParams, totalItems); + + const [skip, setSkip] = useState(getItemsByPage(page)); const sortByDateDirection = searchParams.get(EQueryParams.SORT_BY_DATE); const attester = searchParams.get(EQueryParams.ATTESTER); @@ -37,10 +45,9 @@ export const Attestations: React.FC = () => { ), ); - const { data: attestationsCount } = useSWR( - `${SWRKeys.GET_ATTESTATION_COUNT}/${chain.id}`, - () => sdk.attestation.getAttestationIdCounter() as Promise, - ); + const handlePage = (retrievedPage: number) => { + setSkip(getItemsByPage(retrievedPage)); + }; return (
@@ -51,7 +58,7 @@ export const Attestations: React.FC = () => { {/* TODO: add skeleton for table */} {attestationsList && } - {attestationsCount && } + {attestationsCount && }
); diff --git a/explorer/src/pages/Modules/index.tsx b/explorer/src/pages/Modules/index.tsx new file mode 100644 index 00000000..38f76ae4 --- /dev/null +++ b/explorer/src/pages/Modules/index.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import useSWR from "swr"; + +import { DataTable } from "@/components/DataTable"; +import { Pagination } from "@/components/Pagination"; +import { ITEMS_PER_PAGE_DEFAULT, ZERO } from "@/constants"; +import { columns } from "@/constants/columns/module"; +import { SWRKeys } from "@/interfaces/swr/enum"; +import { useNetworkContext } from "@/providers/network-provider/context"; +import { getItemsByPage, pageBySearchParams } from "@/utils/paginationUtils"; + +export const Modules: React.FC = () => { + const { + sdk, + network: { chain }, + } = useNetworkContext(); + const { data: modulesCount } = useSWR( + `${SWRKeys.GET_MODULE_COUNT}/${chain.id}`, + () => sdk.module.getModulesNumber() as Promise, + ); + + const totalItems = modulesCount ? Number(modulesCount) : ZERO; + const searchParams = new URLSearchParams(window.location.search); + const page = pageBySearchParams(searchParams, totalItems); + + const [skip, setSkip] = useState(getItemsByPage(page)); + + const { data: modulesList } = useSWR(`${SWRKeys.GET_MODULE_LIST}/${skip}/${chain.id}`, () => + sdk.module.findBy(ITEMS_PER_PAGE_DEFAULT, skip), + ); + + const handlePage = (retrievedPage: number) => { + setSkip(getItemsByPage(retrievedPage)); + }; + + return ( +
+
+

Explore Modules

+
+
+ {/* TODO: add skeleton for table */} + {modulesList && } + {Boolean(modulesCount) && } +
+
+ ); +}; diff --git a/explorer/src/routes/index.tsx b/explorer/src/routes/index.tsx index 260f7a84..83252700 100644 --- a/explorer/src/routes/index.tsx +++ b/explorer/src/routes/index.tsx @@ -3,6 +3,7 @@ import { Route, createBrowserRouter, createRoutesFromElements } from "react-rout import { Attestation } from "@/pages/Attestation"; import { Attestations } from "@/pages/Attestations"; import { Home } from "@/pages/Home"; +import { Modules } from "@/pages/Modules"; import { Schema } from "@/pages/Schema"; import { Providers } from "@/providers"; import { loaderNetworkProvider } from "@/providers/network-provider/loader"; @@ -17,6 +18,7 @@ export const router = createBrowserRouter( } /> } /> } /> + } /> } /> , ), diff --git a/explorer/src/utils/paginationUtils.ts b/explorer/src/utils/paginationUtils.ts new file mode 100644 index 00000000..909af41c --- /dev/null +++ b/explorer/src/utils/paginationUtils.ts @@ -0,0 +1,15 @@ +import { CURRENT_PAGE_DEFAULT, ITEMS_PER_PAGE_DEFAULT, ZERO } from "@/constants"; +import { EQueryParams } from "@/enums/queryParams"; + +export const getItemsByPage = (page: number): number => + page === CURRENT_PAGE_DEFAULT ? ZERO : (page - 1) * ITEMS_PER_PAGE_DEFAULT; + +export const pageBySearchParams = ( + searchParams: URLSearchParams, + totalItems: number, + itemsPerPage = ITEMS_PER_PAGE_DEFAULT, +): number => { + const pageSearchParams = searchParams.get(EQueryParams.PAGE); + const totalPages = Math.ceil(Number(totalItems) / itemsPerPage); + return Math.max(1, Math.min(Number(pageSearchParams), totalPages)); +}; diff --git a/explorer/tailwind.config.js b/explorer/tailwind.config.js index 5855799a..5c7af4ce 100644 --- a/explorer/tailwind.config.js +++ b/explorer/tailwind.config.js @@ -37,6 +37,7 @@ export default { blue: "var(--indicator-blue)", magenta: "var(--indicator-magenta)", green: "var(--indicator-green)", + orange: "var(--indicator-orange)", }, hover: { lime20: "var(--hover-lime20)",