Skip to content

Commit

Permalink
feat(explorer): As a user, I want to see a card view of my Attestatio…
Browse files Browse the repository at this point in the history
…ns (#778)

Co-authored-by: Alain Nicolas <[email protected]>
  • Loading branch information
satyajeetkolhapure and alainncls authored Oct 29, 2024
1 parent fc3418b commit 5863e88
Show file tree
Hide file tree
Showing 17 changed files with 513 additions and 162 deletions.
1 change: 1 addition & 0 deletions explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"redirect": "touch dist/_redirects && echo '/* /index.html 200' >> dist/_redirects"
},
"dependencies": {
"@floating-ui/react": "^0.26.25",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@tanstack/react-table": "^8.10.7",
"@verax-attestation-registry/verax-sdk": "2.1.1",
Expand Down
6 changes: 6 additions & 0 deletions explorer/src/assets/icons/circle-info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions explorer/src/components/Tooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
autoUpdate,
flip,
offset,
shift,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
} from "@floating-ui/react";
import { useState } from "react";

interface TooltipProps {
content: React.ReactNode;
children: React.ReactNode;
placement?: "top" | "bottom" | "left" | "right";
isDarkMode?: boolean;
}

export const Tooltip: React.FC<TooltipProps> = ({ content, children, placement = "bottom", isDarkMode = false }) => {
const [isVisible, setIsVisible] = useState(false);
const { refs, floatingStyles, context } = useFloating({
open: isVisible,
onOpenChange: setIsVisible,
placement: placement,
whileElementsMounted: autoUpdate,
middleware: [
offset(5),
flip({
fallbackAxisSideDirection: "start",
}),
shift(),
],
});

const hover = useHover(context, { move: false });
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: "tooltip" });

const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);

return (
<div
className="relative inline-block"
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
ref={refs.setReference}
{...getReferenceProps()}
>
{children}
{isVisible && (
<div
ref={refs.setFloating}
style={{ ...floatingStyles, zIndex: 1000 }}
{...getFloatingProps()}
className={`p-2 rounded-md ${
isDarkMode ? "bg-whiteDefault text-blackDefault" : "bg-blackDefault text-whiteDefault"
}`}
>
{content}
</div>
)}
</div>
);
};
25 changes: 17 additions & 8 deletions explorer/src/constants/columns/attestation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import { Link } from "@/components/Link";
import { SortByDate } from "@/components/SortByDate";
import { ColumnsOptions } from "@/interfaces/components";
import { SWRCell } from "@/pages/Attestations/components/SWRCell";
import { toAttestationById, toPortalById, toSchemaById } from "@/routes/constants";
import { getBlockExplorerLink } from "@/utils";
import {
CHAIN_ID_ROUTE,
toAttestationById,
toAttestationsBySubject,
toPortalById,
toSchemaById,
} from "@/routes/constants";
import { displayAmountWithComma } from "@/utils/amountUtils";
import { cropString } from "@/utils/stringUtils";

Expand All @@ -22,9 +27,14 @@ import { EMPTY_0X_STRING, EMPTY_STRING, ITEMS_PER_PAGE_DEFAULT } from "../index"
interface ColumnsProps {
sortByDate: boolean;
chain: Chain;
network: string;
}

export const columns = ({ sortByDate = true, chain }: Partial<ColumnsProps> = {}): ColumnDef<Attestation>[] => [
export const columns = ({
sortByDate = true,
chain,
network,
}: Partial<ColumnsProps> = {}): ColumnDef<Attestation>[] => [
{
accessorKey: "id",
header: () => (
Expand Down Expand Up @@ -81,14 +91,13 @@ export const columns = ({ sortByDate = true, chain }: Partial<ColumnsProps> = {}
const subjectDisplay = isValidAddress ? <EnsNameDisplay address={subject as Address} /> : cropString(subject);

return (
<a
href={`${getBlockExplorerLink(chain)}/${subject}`}
onClick={(e) => e.stopPropagation()}
target="_blank"
<Link
to={toAttestationsBySubject(subject).replace(CHAIN_ID_ROUTE, network ?? "")}
className="hover:underline"
onClick={(e) => e.stopPropagation()}
>
{subjectDisplay}
</a>
</Link>
);
},
},
Expand Down
125 changes: 125 additions & 0 deletions explorer/src/pages/Attestation/components/AttestationCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { ChevronRight } from "lucide-react";
import moment from "moment";
import { generatePath, useLocation, useNavigate } from "react-router-dom";
import { useTernaryDarkMode } from "usehooks-ts";

import circleInfo from "@/assets/icons/circle-info.svg";
import { Button } from "@/components/Buttons";
import { EButtonType } from "@/components/Buttons/enum";
import { Tooltip } from "@/components/Tooltip";
import { issuersData } from "@/pages/Home/data";
import { IIssuer } from "@/pages/Home/interface";
import { useNetworkContext } from "@/providers/network-provider/context";
import { APP_ROUTES } from "@/routes/constants";

import { IAttestationCardProps } from "./interface";

export const AttestationCard: React.FC<IAttestationCardProps> = ({
id,
schemaId,
portalId,
issuanceDate,
expiryDate,
}) => {
const {
network: { network },
} = useNetworkContext();
const navigate = useNavigate();
const location = useLocation();
const { isDarkMode } = useTernaryDarkMode();
const isExpired = expiryDate ? new Date(expiryDate * 1000) < new Date() : false;

const issuerData = issuersData.find((issuer) =>
issuer.attestationDefinitions.some(
(definition) =>
definition.schema.toLowerCase() === schemaId.toLowerCase() &&
definition.portal.toLowerCase() === portalId.toLowerCase(),
),
) as IIssuer;
const attestationDefinitions = issuerData?.attestationDefinitions.find(
(definition) => definition.schema.toLowerCase() === schemaId.toLowerCase(),
);

if (!issuerData) {
return null;
}

const logo = attestationDefinitions?.logo ?? issuerData?.logo;
const logoDark = attestationDefinitions?.logoDark ?? issuerData?.logoDark;
const name = attestationDefinitions?.name ?? issuerData?.name;
const description = attestationDefinitions?.description ?? "";
const issuerName = issuerData.name;

const maxDescriptionLength = 140;
const isDescriptionLong = description.length > maxDescriptionLength;
const truncatedDescription = isDescriptionLong ? `${description.slice(0, maxDescriptionLength)}...` : description;

const handleViewDetailsClick = (id: string) => {
navigate(generatePath(APP_ROUTES.ATTESTATION_BY_ID, { chainId: network, id }), {
state: { from: location.pathname },
});
};

const displayLogo = () => {
const Logo: React.FC<React.SVGProps<SVGSVGElement>> = isDarkMode && logoDark ? logoDark : logo;
return <Logo className="w-full h-auto max-w-[2.5rem] md:max-w-[3rem] max-h-[2.5rem] md:max-h-[3rem]" />;
};

return (
<div
key={`${id}`}
className="group flex flex-col justify-between gap-4 border border-border-card dark:border-border-cardDark rounded-xl p-4 md:p-6 hover:bg-surface-secondary dark:hover:bg-surface-secondaryDark transition md:min-h-[20rem]"
>
<div>
<div className="flex items-start gap-3 text-xl md:text-md font-semibold text-blackDefault dark:text-whiteDefault">
<div className="w-[2.5rem] h-[2.5rem] md:w-[3rem] md:h-[3rem] flex items-center mr-2 justify-center">
{displayLogo()}
</div>
<div className="flex flex-col">
<div>{name}</div>
<div className="text-sm font-normal text-blackDefault dark:text-whiteDefault">{issuerName}</div>
</div>
</div>
{description && description.trim() ? (
<div className="text-sm font-normal text-text-darkGrey dark:text-tertiary mt-4">
<span>{truncatedDescription}</span>
</div>
) : null}
</div>
<div className="flex flex-col gap-2 mt-auto">
<div className="flex justify-between text-sm font-normal text-text-darkGrey dark:text-tertiary">
<span>Issued</span> <span>{moment.unix(issuanceDate).fromNow()}</span>
</div>
{!!expiryDate && isExpired && (
<div className="flex justify-between text-sm font-semibold text-text-darkGrey dark:text-tertiary">
<div className="flex items-center">
<span>Expired</span>
<Tooltip
content={
<div style={{ width: "350px", fontWeight: "normal" }}>
The validity of this Attestation is determined by the Issuer, and consumers may choose to adhere to
or ignore this expiration date.
</div>
}
placement="right"
isDarkMode={isDarkMode}
>
<img src={circleInfo} className="!h-[16px] !w-[16px] ml-1" />
</Tooltip>
</div>
<span>{moment.unix(expiryDate).fromNow()}</span>
</div>
)}
<div className="flex mt-4 lg:flex-row lg:items-end justify-end lg:justify-start">
<Button
isSmall
name="View details"
handler={() => handleViewDetailsClick(id)}
buttonType={EButtonType.OUTLINED}
iconRight={<ChevronRight />}
/>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface IAttestationCardProps {
id: string;
schemaId: string;
portalId: string;
issuanceDate: number;
expiryDate?: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useEnsName } from "wagmi";

import { Link } from "@/components/Link";
import { useNetworkContext } from "@/providers/network-provider/context";
import { toPortalById } from "@/routes/constants";
import { CHAIN_ID_ROUTE, toAttestationsBySubject, toPortalById } from "@/routes/constants";
import { getBlockExplorerLink } from "@/utils";
import { displayAmountWithComma } from "@/utils/amountUtils";
import { cropString } from "@/utils/stringUtils";
Expand All @@ -17,7 +17,7 @@ import { createDateListItem } from "./utils";

export const AttestationInfo: React.FC<Attestation> = ({ ...attestation }) => {
const {
network: { chain },
network: { chain, network },
} = useNetworkContext();

const { data: attesterEnsAddress } = useEnsName({
Expand Down Expand Up @@ -73,7 +73,7 @@ export const AttestationInfo: React.FC<Attestation> = ({ ...attestation }) => {
{
title: t("attestation.info.subject"),
value: displaySubjectEnsNameOrAddress(),
link: `${blockExplorerLink}/${subject}`,
to: toAttestationsBySubject(subject).replace(CHAIN_ID_ROUTE, network),
},
];

Expand Down
24 changes: 24 additions & 0 deletions explorer/src/pages/Attestations/components/CardView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AttestationCard } from "@/pages/Attestation/components/AttestationCard";

import { ICardViewProps } from "./interface";

export const CardView: React.FC<ICardViewProps> = ({ attestationsList }) => {
return (
<div className="flex flex-col gap-14 md:gap-[4.5rem] container mt-14 md:mt-12">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{attestationsList?.map((attestation) => {
return (
<AttestationCard
key={attestation.id}
id={attestation.id}
schemaId={attestation.schema.id}
portalId={attestation.portal.id}
issuanceDate={attestation.attestedDate}
expiryDate={attestation.expirationDate}
/>
);
})}
</div>
</div>
);
};
13 changes: 13 additions & 0 deletions explorer/src/pages/Attestations/components/CardView/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface ICardViewProps {
attestationsList: Array<{
id: string;
schema: {
id: string;
};
portal: {
id: string;
};
attestedDate: number;
expirationDate?: number;
}>;
}
2 changes: 1 addition & 1 deletion explorer/src/pages/Attestations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const Attestations: React.FC = () => {

const data = isLoading
? { columns: columnsSkeletonRef.current, list: skeletonAttestations(itemsPerPage) }
: { columns: columns({ chain: network.chain }), list: attestationsList || [] };
: { columns: columns({ chain: network.chain, network: network.network }), list: attestationsList || [] };

const renderPagination = () => {
if (attestationsCount) {
Expand Down
6 changes: 2 additions & 4 deletions explorer/src/pages/MyAttestations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ import { useAccount } from "wagmi";

import { Button } from "@/components/Buttons";
import { EButtonType } from "@/components/Buttons/enum";
import { DataTable } from "@/components/DataTable";
import { InfoBlock } from "@/components/InfoBlock";
import { THOUSAND } from "@/constants";
import { columns } from "@/constants/columns/attestation";
import { EQueryParams } from "@/enums/queryParams";
import useWindowDimensions from "@/hooks/useWindowDimensions";
import { SWRKeys } from "@/interfaces/swr/enum";
import { useNetworkContext } from "@/providers/network-provider/context";
import { APP_ROUTES } from "@/routes/constants";
import { cropString } from "@/utils/stringUtils";

import { CardView } from "../Attestations/components/CardView";
import { TitleAndSwitcher } from "../Attestations/components/TitleAndSwitcher";

export const MyAttestations: React.FC = () => {
Expand Down Expand Up @@ -91,7 +89,7 @@ export const MyAttestations: React.FC = () => {
) : !attestationsList || !attestationsList.length ? (
<InfoBlock icon={<ArchiveIcon />} message={t("attestation.messages.emptyList")} />
) : (
<DataTable columns={columns({ chain })} data={attestationsList} link={APP_ROUTES.ATTESTATION_BY_ID} />
<CardView attestationsList={attestationsList}></CardView>
)}
</TitleAndSwitcher>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { APP_ROUTES } from "@/routes/constants";
export const RecentAttestations: React.FC<{ schemaId: string }> = ({ schemaId }) => {
const {
sdk,
network: { chain },
network: { chain, network },
} = useNetworkContext();

const { data: attestations, isLoading } = useSWR(
Expand All @@ -27,7 +27,7 @@ export const RecentAttestations: React.FC<{ schemaId: string }> = ({ schemaId })
const data = isLoading
? { columns: columnsSkeletonRef.current, list: skeletonAttestations(5) }
: {
columns: columns({ sortByDate: false, chain }),
columns: columns({ sortByDate: false, chain, network }),
list: attestations || [],
};

Expand Down
Loading

0 comments on commit 5863e88

Please sign in to comment.