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(explorer): As a user, I want to see a card view of my Attestations #778

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
);
};
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
Loading