Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: As a user, I want to be able to connect my wallet to Explorer and see the list of my attestations #456

Merged
3 changes: 2 additions & 1 deletion explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"prettier": "^3.1.0",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2",
"vite": "^4.5.0"
"vite": "^4.5.0",
"vite-plugin-svgr": "^4.2.0"
}
}
12 changes: 12 additions & 0 deletions explorer/src/components/Buttons/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IButtonsProps } from "./interface";

export const ButtonOutlined: React.FC<IButtonsProps> = ({ name, handler }) => {
return (
<button
onClick={handler}
className="h-12 px-4 py-3 rounded-md border border-zinc-200 justify-center items-center gap-2 inline-flex text-zinc-950 text-base font-semibold hover:border-slate-500 disabled:opacity-40"
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
>
{name}
</button>
);
};
4 changes: 4 additions & 0 deletions explorer/src/components/Buttons/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IButtonsProps {
name: string;
handler(): void;
}
12 changes: 6 additions & 6 deletions explorer/src/components/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import hapi from "@/assets/logo/hapi.svg";
import verax from "@/assets/logo/verax.svg";
import HapiLogo from "@/assets/logo/hapi.svg";
import VeraxLogo from "@/assets/logo/verax.svg";
import { Link } from "@/components/Link";
import { INFO_LIST } from "@/constants/components";
import { APP_ROUTES } from "@/routes/constants";
Expand All @@ -8,23 +8,23 @@ export const Footer: React.FC = () => {
return (
<footer className="flex flex-col justify-between items-center py-5 sm:px-8 md:px-[60px] border-t-[1px] border-border-table sm:flex-row gap-14 sm:gap-0 transition-spacing">
<Link to={APP_ROUTES.HOME}>
<img src={verax} alt="verax" />
<VeraxLogo />
</Link>
<div className="flex flex-col sm:flex-row gap-6 sm:gap-8 text-text-quaternary text-sm">
{INFO_LIST.map(({ title, logo, url }) => (
<a
key={title}
href={url}
target="_blank"
className="flex justify-center self-center gap-2 hover:underline hover:text-zinc-950"
className="flex justify-center items-center self-center gap-2 hover:underline hover:text-zinc-950"
>
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
{logo && <img src={logo} alt={title} className="!w-4 !h-4 self-center" />}
{logo && logo}
{title}
</a>
))}
</div>
<a href={"https://hapi.one/"} target="_blank">
<img src={hapi} alt="hapi" className="!h-[25px]" />
<HapiLogo />
</a>
</footer>
);
Expand Down
8 changes: 4 additions & 4 deletions explorer/src/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ConnectKitButton } from "connectkit";
import { ChevronDown } from "lucide-react";
import { Dispatch, SetStateAction } from "react";

import logo from "@/assets/logo/header-logo.svg";
import VeraxLogo from "@/assets/logo/header-logo.svg";
import { Link } from "@/components/Link";
import {
DropdownMenu,
Expand Down Expand Up @@ -34,14 +34,14 @@ export const Header: React.FC<HeaderProps> = ({ isOpened, setIsOpened }) => {
<header className="px-5 md:px-14 xl:px-[60px] py-3 justify-between items-center inline-flex">
<div className="justify-start items-center gap-6 flex self-stretch">
<Link to={APP_ROUTES.HOME} className="shrink-0 hover:opacity-70">
<img src={logo} className="h-6 xl:h-9 cursor-pointer" alt="Verax logo" />
<VeraxLogo />
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
</Link>
{!isAdaptive && <NavigationList />}
</div>
<div className="justify-start items-center gap-4 flex">
<DropdownMenu>
<DropdownMenuTrigger className="DropdownMenuTrigger select-none w-[72px] p-2 rounded-md outline-none hover:bg-hover-lime20 justify-start items-center gap-2 inline-flex">
<img src={network.img} className="w-6 h-6 relative" alt="Linea logo" />
{network.img}
<ChevronDown className="header-arrow w-6 h-6 relative" />
</DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-2 bg-surface-primary">
Expand All @@ -51,7 +51,7 @@ export const Header: React.FC<HeaderProps> = ({ isOpened, setIsOpened }) => {
className="flex gap-2 focus:bg-hover-lime20 cursor-pointer"
onClick={() => setNetwork(chain)}
>
<img src={chain.img} className="w-6 h-6" alt={chain.name} />
{chain.img}
{chain.name}
</DropdownMenuItem>
))}
Expand Down
13 changes: 13 additions & 0 deletions explorer/src/components/InfoBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IInfoBlockProps } from "./interface";
import { ButtonOutlined } from "../Buttons";

export const InfoBlock: React.FC<IInfoBlockProps> = ({ message, button, buttonComponent, icon }) => {
return (
<div className="h-[27.625rem] md:h-[40.875rem] flex flex-col items-center justify-center gap-6">
{icon}
<div className="text-slate-500 text-base font-normal">{message}</div>
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
{button && <ButtonOutlined name={button.name} handler={button.handler} />}
{buttonComponent && buttonComponent}
</div>
);
};
8 changes: 8 additions & 0 deletions explorer/src/components/InfoBlock/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IButtonsProps } from "../Buttons/interface";

export interface IInfoBlockProps {
icon: JSX.Element;
message: string;
button?: IButtonsProps;
buttonComponent?: JSX.Element;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import { createConfig } from "wagmi";
import { linea, lineaTestnet } from "wagmi/chains";

import veraxColoredIcon from "@/assets/logo/verax-colored-icon.svg";
import lineaMainnetIcon from "@/assets/networks/linea-mainnet.svg";
import lineaTestnetIcon from "@/assets/networks/linea-testnet.svg";
import LineaMainnetIcon from "@/assets/networks/linea-mainnet.svg";
import LineaTestnetIcon from "@/assets/networks/linea-testnet.svg";
import { INetwork } from "@/interfaces/config";

const chains: INetwork[] = [
{
name: "Linea Mainnet",
chain: linea,
veraxEnv: VeraxSdk.DEFAULT_LINEA_MAINNET_FRONTEND,
img: lineaMainnetIcon,
img: <LineaMainnetIcon />,
network: "linea",
},
{
name: "Linea Testnet",
chain: lineaTestnet,
veraxEnv: VeraxSdk.DEFAULT_LINEA_TESTNET_FRONTEND,
img: lineaTestnetIcon,
img: <LineaTestnetIcon />,
network: "linea-testnet",
},
];
Expand Down
12 changes: 6 additions & 6 deletions explorer/src/constants/components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import gitbook from "@/assets/icons/gitbook.svg";
import veraxIcon from "@/assets/logo/verax-icon.svg";
import github from "@/assets/socials/github.svg";
import GitbookIcon from "@/assets/icons/gitbook.svg";
import VeraxIcon from "@/assets/logo/verax-icon.svg";
import GithubIcon from "@/assets/socials/github.svg";
import { Info } from "@/components/NavigationList/components/Info";
import { NavigationProps } from "@/interfaces/components";
import { APP_ROUTES } from "@/routes/constants";
Expand Down Expand Up @@ -31,17 +31,17 @@ export const DEFAULT_ROUTES: Array<NavigationProps> = [
export const INFO_LIST = [
{
title: "About",
logo: veraxIcon,
logo: <VeraxIcon />,
url: "https://ver.ax/",
},
{
title: "Github",
logo: github,
logo: <GithubIcon />,
url: "https://github.com/Consensys/linea-attestation-registry/tree/dev",
},
{
title: "Documentation",
logo: gitbook,
logo: <GitbookIcon />,
url: "https://docs.ver.ax/verax-documentation/",
},
];
2 changes: 1 addition & 1 deletion explorer/src/enums/queryParams.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export enum EQueryParams {
PAGE = "page",
SORT_BY_DATE = "sort_by_date",
ATTESTER = "attester",
SHOW_MY_ATTESTATIONS = "show_my_attestations",
}
2 changes: 1 addition & 1 deletion explorer/src/interfaces/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export interface INetwork {
name: string;
chain: Chain;
veraxEnv: Conf;
img: string;
img: JSX.Element;
network: string;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Attestation } from "@verax-attestation-registry/verax-sdk";
import { EyeOffIcon } from "lucide-react";
import { Link } from "react-router-dom";
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
import { KeyedMutator } from "swr";
import { Hex, hexToNumber } from "viem";

import { Link } from "@/components/Link";
import { useNetworkContext } from "@/providers/network-provider/context";
import { toAttestationById } from "@/routes/constants";
import { displayAmountWithComma } from "@/utils/amountUtils";
Expand Down
31 changes: 15 additions & 16 deletions explorer/src/pages/Attestations/components/ListSwitcher/index.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
import { useSearchParams } from "react-router-dom";
import { useAccount } from "wagmi";

import { EQueryParams } from "@/enums/queryParams";

export const ListSwitcher = () => {
const { address } = useAccount();
export const ListSwitcher: React.FC = () => {
const searchParams = new URLSearchParams(window.location.search);
const isMyAttestations = searchParams.get(EQueryParams.SHOW_MY_ATTESTATIONS);

const [searchParams, setSearchParams] = useSearchParams();
const [, setSearchParams] = useSearchParams();

const attester = searchParams.get(EQueryParams.ATTESTER);
const handleShowAttestations = (showMy?: boolean) => {
const currentSearchParams = new URLSearchParams();

const handleAttester = (address?: string) => {
const currentSearchParams = new URLSearchParams(searchParams);
address
? currentSearchParams.set(EQueryParams.ATTESTER, address)
: currentSearchParams.delete(EQueryParams.ATTESTER);
showMy
? currentSearchParams.set(EQueryParams.SHOW_MY_ATTESTATIONS, "true")
: currentSearchParams.delete(EQueryParams.SHOW_MY_ATTESTATIONS);

setSearchParams(currentSearchParams);
};

return (
<div className="inline-flex bg-slate-100 gap-2 mb-6 rounded">
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
<button
disabled={Boolean(!attester)}
onClick={() => handleAttester()}
disabled={!isMyAttestations}
onClick={() => handleShowAttestations()}
className={`h-[2.1875rem] px-3 rounded text-base font-medium ${
attester ? "text-text-tertiary" : "text-white bg-gray-700"
isMyAttestations ? "text-text-tertiary" : "text-white bg-gray-700"
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
}`}
>
All attestations
</button>
<button
disabled={!address || Boolean(attester)}
onClick={() => handleAttester(address)}
disabled={!!isMyAttestations}
onClick={() => handleShowAttestations(true)}
className={`h-[2.1875rem] px-3 rounded text-base font-medium ${
attester ? "text-white bg-gray-700" : "text-text-tertiary"
isMyAttestations ? "text-white bg-gray-700" : "text-text-tertiary"
Solniechniy marked this conversation as resolved.
Show resolved Hide resolved
}`}
>
My attestations
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ConnectKitButton } from "connectkit";
import { useSearchParams } from "react-router-dom";
import { useAccount } from "wagmi";

import ArchiveIcon from "@/assets/icons/archive.svg";
import { ButtonOutlined } from "@/components/Buttons";
import { InfoBlock } from "@/components/InfoBlock";
import { EQueryParams } from "@/enums/queryParams";

import { IMyAttestations } from "./interface";
import { columns } from "../../table/columns";
import { DataTable } from "../../table/dataTable";

export const MyAttestations: React.FC<IMyAttestations> = ({ attestationsList }) => {
const { address } = useAccount();
const [searchParams] = useSearchParams();

const isMyAttestations = searchParams.get(EQueryParams.SHOW_MY_ATTESTATIONS);

if (!address && isMyAttestations)
return (
<InfoBlock
icon={<ArchiveIcon />}
message="Connect wallet to see your attestations"
buttonComponent={
<ConnectKitButton.Custom>
{({ show }) => {
if (!show) return <></>;
return <ButtonOutlined name="Connect Wallet" handler={show} />;
}}
</ConnectKitButton.Custom>
}
/>
);

if (address && isMyAttestations && !attestationsList?.length)
return <InfoBlock icon={<ArchiveIcon />} message="You don’t have any attestations yet" />;

return <>{address && isMyAttestations && <DataTable columns={columns} data={attestationsList || []} />}</>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Attestation } from "@verax-attestation-registry/verax-sdk";

export interface IMyAttestations {
attestationsList: Attestation[] | undefined;
}
27 changes: 18 additions & 9 deletions explorer/src/pages/Attestations/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { OrderDirection } from "@verax-attestation-registry/verax-sdk/lib/types/.graphclient";
import { useState } from "react";
import useSWR from "swr";
import { useAccount } from "wagmi";

import { DataTable } from "@/components/DataTable";
import { Pagination } from "@/components/Pagination";
import { ITEMS_PER_PAGE_DEFAULT, ZERO } from "@/constants";
import { EMPTY_STRING, ITEMS_PER_PAGE_DEFAULT, ZERO } from "@/constants";
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";
import { MyAttestations } from "./components/MyAttestations";
import { DataTable } from "./table/dataTable";

export const Attestations: React.FC = () => {
const {
sdk,
network: { chain },
} = useNetworkContext();
const { address } = useAccount();

const { data: attestationsCount } = useSWR(
`${SWRKeys.GET_ATTESTATION_COUNT}/${chain.id}`,
Expand All @@ -27,19 +30,20 @@ export const Attestations: React.FC = () => {
const totalItems = attestationsCount ?? ZERO;
const searchParams = new URLSearchParams(window.location.search);
const page = pageBySearchParams(searchParams, totalItems);
const sortByDateDirection = searchParams.get(EQueryParams.SORT_BY_DATE);
const isMyAttestations = searchParams.get(EQueryParams.SHOW_MY_ATTESTATIONS);

const [skip, setSkip] = useState<number>(getItemsByPage(page));

const sortByDateDirection = searchParams.get(EQueryParams.SORT_BY_DATE);
const attester = searchParams.get(EQueryParams.ATTESTER);

const { data: attestationsList } = useSWR(
`${SWRKeys.GET_ATTESTATION_LIST}/${skip}/${attester}/${sortByDateDirection}/${chain.id}`,
`${SWRKeys.GET_ATTESTATION_LIST}/${skip}/${isMyAttestations ? address : EMPTY_STRING}/${sortByDateDirection}/${
chain.id
}`,
() =>
sdk.attestation.findBy(
ITEMS_PER_PAGE_DEFAULT,
skip,
attester ? { attester } : undefined,
isMyAttestations ? { attester: address } : undefined,
"attestedDate",
sortByDateDirection as OrderDirection,
),
Expand All @@ -57,8 +61,13 @@ export const Attestations: React.FC = () => {
<div>
<ListSwitcher />
{/* TODO: add skeleton for table */}
{attestationsList && <DataTable columns={columns()} data={attestationsList} />}
{attestationsCount && <Pagination itemsCount={attestationsCount} handlePage={handlePage} />}
<MyAttestations attestationsList={attestationsList} />
{!isMyAttestations && (
<>
<DataTable columns={columns()} data={attestationsList || []} />
{attestationsCount && <Pagination itemsCount={attestationsCount} handlePage={handlePage} />}
</>
)}
</div>
</div>
);
Expand Down
Loading
Loading