diff --git a/explorer/src/assets/issuers/Hapi_logo_square.svg b/explorer/src/assets/issuers/hapi.svg similarity index 100% rename from explorer/src/assets/issuers/Hapi_logo_square.svg rename to explorer/src/assets/issuers/hapi.svg diff --git a/explorer/src/assets/issuers/index-network-black.svg b/explorer/src/assets/issuers/index-network-black.svg deleted file mode 100644 index 5f1405f2..00000000 --- a/explorer/src/assets/issuers/index-network-black.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/explorer/src/assets/issuers/index-network.svg b/explorer/src/assets/issuers/index-network.svg deleted file mode 100644 index ea6e6834..00000000 --- a/explorer/src/assets/issuers/index-network.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/explorer/src/assets/issuers/gitcoin.svg b/explorer/src/assets/issuers/passport-xyz.svg similarity index 100% rename from explorer/src/assets/issuers/gitcoin.svg rename to explorer/src/assets/issuers/passport-xyz.svg diff --git a/explorer/src/assets/locales/en/en.json b/explorer/src/assets/locales/en/en.json index b156d5fb..2dc1fc4b 100644 --- a/explorer/src/assets/locales/en/en.json +++ b/explorer/src/assets/locales/en/en.json @@ -17,7 +17,9 @@ "next": "Next", "learnMore": "Learn More", "getStarted": "Get Started", - "viewMyAttestations": "View My Attestations" + "viewMyAttestations": "View My Attestations", + "getAttestation": "Get Attestations", + "seeAttestations": "See Attestations" }, "messages": { "empty": "Empty", diff --git a/explorer/src/assets/networks/base-mainnet.svg b/explorer/src/assets/networks/base.svg similarity index 100% rename from explorer/src/assets/networks/base-mainnet.svg rename to explorer/src/assets/networks/base.svg diff --git a/explorer/src/assets/networks/bsc-mainnet.svg b/explorer/src/assets/networks/bsc.svg similarity index 100% rename from explorer/src/assets/networks/bsc-mainnet.svg rename to explorer/src/assets/networks/bsc.svg diff --git a/explorer/src/assets/networks/linea-mainnet-dark.svg b/explorer/src/assets/networks/linea-dark.svg similarity index 100% rename from explorer/src/assets/networks/linea-mainnet-dark.svg rename to explorer/src/assets/networks/linea-dark.svg diff --git a/explorer/src/assets/networks/linea-mainnet.svg b/explorer/src/assets/networks/linea.svg similarity index 100% rename from explorer/src/assets/networks/linea-mainnet.svg rename to explorer/src/assets/networks/linea.svg diff --git a/explorer/src/components/Pagination/Basic/index.tsx b/explorer/src/components/Pagination/Basic/index.tsx new file mode 100644 index 00000000..4b19c15e --- /dev/null +++ b/explorer/src/components/Pagination/Basic/index.tsx @@ -0,0 +1,92 @@ +import { t } from "i18next"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { useSearchParams } from "react-router-dom"; + +import { ITEMS_PER_PAGE_DEFAULT } from "@/constants"; +import { EQueryParams } from "@/enums/queryParams"; + +import { IBasicPaginationProps } from "./interface"; +import { PerPageSelector } from "../PerPageSelector"; + +export const BasicPagination = ({ handlePage }: IBasicPaginationProps) => { + const [searchParams, setSearchParams] = useSearchParams(); + const [itemsPerPage, setItemsPerPage] = useState( + Number(searchParams.get(EQueryParams.ITEMS_PER_PAGE)) || ITEMS_PER_PAGE_DEFAULT, + ); + const [currentPage, setCurrentPage] = useState(1); + + useEffect(() => { + handlePage(currentPage); + }, [currentPage, handlePage, searchParams]); + + const itemsPerPageValues = [ITEMS_PER_PAGE_DEFAULT, 20, 50, 100]; + + const inputRef = useRef(null); + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && inputRef && inputRef.current) { + inputRef.current.value = newPage.toString(); + searchParams.set(EQueryParams.PAGE, newPage.toString()); + setSearchParams(searchParams); + setCurrentPage(newPage); + } + }; + + const handleItemsPerPage = (val: number | string) => { + setItemsPerPage(val); + searchParams.set(EQueryParams.ITEMS_PER_PAGE, String(val)); + setSearchParams(searchParams); + }; + const handlePreviousPage = () => handlePageChange(currentPage - 1); + const handleNextPage = () => handlePageChange(currentPage + 1); + + const changePage = (inputPage: string) => { + const page = Number(inputPage); + inputPage.length && handlePageChange(page); + }; + + const blurHandler = () => { + !inputRef.current?.value.length && handlePageChange(currentPage); + }; + + return ( +
+
+ +
+
+ changePage(event.target.value)} + className="w-16 h-8 px-2 border text-xs font-semibold dark:bg-transparent text-text-primary dark:text-whiteDefault text-center outline-none border-border-table dark:border-greyDark focus:border-border-inputFocus dark:focus:border-border-inputFocus rounded-lg transition" + /> + + + {t("common.messages.perPage")} + +
+
+ +
+
+ ); +}; diff --git a/explorer/src/components/Pagination/Basic/interface.ts b/explorer/src/components/Pagination/Basic/interface.ts new file mode 100644 index 00000000..9b1c0c8a --- /dev/null +++ b/explorer/src/components/Pagination/Basic/interface.ts @@ -0,0 +1,3 @@ +export interface IBasicPaginationProps { + handlePage: (page: number) => void; +} diff --git a/explorer/src/config/index.tsx b/explorer/src/config/index.tsx index 0cb0a867..4dd8fcf1 100644 --- a/explorer/src/config/index.tsx +++ b/explorer/src/config/index.tsx @@ -10,14 +10,14 @@ import ArbitrumNovaIcon from "@/assets/networks/arbitrum-nova.svg?react"; import ArbitrumSepoliaIcon from "@/assets/networks/arbitrum-sepolia.svg?react"; import ArbitrumIcon from "@/assets/networks/arbitrum.svg?react"; import BaseIconDark from "@/assets/networks/base-dark.svg?react"; -import BaseMainnetIcon from "@/assets/networks/base-mainnet.svg?react"; import BaseSepoliaIcon from "@/assets/networks/base-sepolia.svg?react"; +import BaseMainnetIcon from "@/assets/networks/base.svg?react"; import BscMainnetIconDark from "@/assets/networks/bsc-dark.svg?react"; -import BscMainnetIcon from "@/assets/networks/bsc-mainnet.svg?react"; import BscTestnetIcon from "@/assets/networks/bsc-testnet.svg?react"; -import LineaMainnetIconDark from "@/assets/networks/linea-mainnet-dark.svg?react"; -import LineaMainnetIcon from "@/assets/networks/linea-mainnet.svg?react"; +import BscMainnetIcon from "@/assets/networks/bsc.svg?react"; +import LineaMainnetIconDark from "@/assets/networks/linea-dark.svg?react"; import LineaSepoliaIcon from "@/assets/networks/linea-sepolia.svg?react"; +import LineaMainnetIcon from "@/assets/networks/linea.svg?react"; import { INetwork } from "@/interfaces/config"; const lineaSepolia = { diff --git a/explorer/src/enums/queryParams.ts b/explorer/src/enums/queryParams.ts index 597ed184..6034791d 100644 --- a/explorer/src/enums/queryParams.ts +++ b/explorer/src/enums/queryParams.ts @@ -3,4 +3,5 @@ export enum EQueryParams { SORT_BY_DATE = "sort_by_date", SEARCH_QUERY = "search_query", ITEMS_PER_PAGE = "page_size", + WHERE = "where", } diff --git a/explorer/src/pages/Attestations/index.tsx b/explorer/src/pages/Attestations/index.tsx index 0f8c5dd2..b36c08cf 100644 --- a/explorer/src/pages/Attestations/index.tsx +++ b/explorer/src/pages/Attestations/index.tsx @@ -1,10 +1,11 @@ -import { OrderDirection } from "@verax-attestation-registry/verax-sdk/lib/types/.graphclient"; +import { Attestation_filter, OrderDirection } from "@verax-attestation-registry/verax-sdk/lib/types/.graphclient"; import { useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; import useSWR from "swr"; import { DataTable } from "@/components/DataTable"; import { Pagination } from "@/components/Pagination"; +import { BasicPagination } from "@/components/Pagination/Basic"; import { ITEMS_PER_PAGE_DEFAULT, ZERO } from "@/constants"; import { attestationColumnsOption, columns, skeletonAttestations } from "@/constants/columns/attestation"; import { columnsSkeleton } from "@/constants/columns/skeleton"; @@ -32,6 +33,9 @@ export const Attestations: React.FC = () => { const page = pageBySearchParams(searchParams, totalItems); const sortByDateDirection = searchParams.get(EQueryParams.SORT_BY_DATE); const itemsPerPage = Number(searchParams.get(EQueryParams.ITEMS_PER_PAGE)) || ITEMS_PER_PAGE_DEFAULT; + const where = searchParams.get(EQueryParams.WHERE) + ? (JSON.parse(searchParams.get(EQueryParams.WHERE) ?? "") as Attestation_filter) + : undefined; const [lastID, setLastID] = useState(getItemsByPage(page, itemsPerPage)); @@ -43,11 +47,14 @@ export const Attestations: React.FC = () => { sdk.attestation.findBy( itemsPerPage, undefined, - (sortByDateDirection as OrderDirection) === null || + { + ...where, + ...((sortByDateDirection as OrderDirection) === null || (sortByDateDirection as OrderDirection) === undefined || (sortByDateDirection as OrderDirection) === ETableSorting.DESC - ? { id_lte: buildAttestationId(totalItems - (page - 1) * itemsPerPage, network.prefix) } - : { id_gt: buildAttestationId(lastID, network.prefix) }, + ? { id_lte: buildAttestationId(totalItems - (page - 1) * itemsPerPage, network.prefix) } + : { id_gt: buildAttestationId(lastID, network.prefix) }), + }, "attestedDate", (sortByDateDirection as OrderDirection) || ETableSorting.DESC, ), @@ -62,10 +69,22 @@ export const Attestations: React.FC = () => { ? { columns: columnsSkeletonRef.current, list: skeletonAttestations(itemsPerPage) } : { columns: columns({ chain: network.chain }), list: attestationsList || [] }; + const renderPagination = () => { + if (attestationsCount) { + if (where) { + return ; + } else { + return ; + } + } else { + return null; + } + }; + return ( - {attestationsCount ? : null} + {renderPagination()} ); }; diff --git a/explorer/src/pages/Home/components/Issuers/index.tsx b/explorer/src/pages/Home/components/Issuers/index.tsx index 64175433..046e0659 100644 --- a/explorer/src/pages/Home/components/Issuers/index.tsx +++ b/explorer/src/pages/Home/components/Issuers/index.tsx @@ -4,9 +4,11 @@ import { Issuer } from "../Issuer"; export const Issuers: React.FC = () => { return (
- {issuersData.map((issuer) => ( - - ))} + {issuersData + .sort((a, b) => a.name.localeCompare(b.name)) + .map((issuer) => ( + + ))}
); }; diff --git a/explorer/src/pages/Home/data.tsx b/explorer/src/pages/Home/data.tsx index 38c671c4..672c82f1 100644 --- a/explorer/src/pages/Home/data.tsx +++ b/explorer/src/pages/Home/data.tsx @@ -1,10 +1,7 @@ import ZeroXScore from "@/assets/issuers/0xscore.svg?react"; import Aspecta from "@/assets/issuers/aspecta.svg?react"; import Automata from "@/assets/issuers/automata.svg?react"; -import Gitcoin from "@/assets/issuers/gitcoin.svg?react"; -import Hapi from "@/assets/issuers/Hapi_logo_square.svg?react"; -import IndexNetwork from "@/assets/issuers/index-network-black.svg?react"; -import IndexNetworkDark from "@/assets/issuers/index-network.svg?react"; +import Hapi from "@/assets/issuers/hapi.svg?react"; import Nomis from "@/assets/issuers/nomis.svg?react"; import Okapi from "@/assets/issuers/okapi-black.svg?react"; import OkapiDark from "@/assets/issuers/okapi-white.svg?react"; @@ -12,6 +9,7 @@ import OpenId3 from "@/assets/issuers/openid3.svg?react"; import Orange from "@/assets/issuers/orange.svg?react"; import PadoDark from "@/assets/issuers/pado-dark.svg?react"; import Pado from "@/assets/issuers/pado.svg?react"; +import PassportXyz from "@/assets/issuers/passport-xyz.svg?react"; import PrivadoID from "@/assets/issuers/privado-id.svg?react"; import Reclaim from "@/assets/issuers/reclaim.svg?react"; import RubyScore from "@/assets/issuers/rubyscore.svg?react"; @@ -34,16 +32,50 @@ export const issuersData: IIssuer[] = [ "https://trustgo.trustalabs.ai/etrusta/0x085ed975a8b6b860de3c2b871da60a3f9f48a5b8/lineaverax/h?f=linea&chainId=324", CTATitle: "Go To Trusta Labs", address: "0x9e728394E55e6535BF66f913e911Ae1f572D8db0", + attestationDefinitions: [ + { + name: "Reputation attestation", + logo: Trusta, + description: + "Trusta's Sybil Score attestation provides a simple, permissionless, and privacy-preserving way to verify your humanity based on machine-learning algorithm", + portal: "0xb86b3e16b6b960fd822849fd4b4861d73805879b", + schema: "0x63c8925414d0bd8e4b943b49867adef6fabf8fb66d9ecefacfa90272623edf9e", + url: "https://trustgo.trustalabs.ai/etrusta/0x085ed975a8b6b860de3c2b871da60a3f9f48a5b8/lineaverax/m?chainId=324&s=EPR0QD9W52I8", + chainId: "0xe708", + }, + { + name: "Humanity attestation", + logo: Trusta, + description: + "Trusta's MEDIA Score attestation provides a simple, quantifiable and privacy-preserving way to evaluate your reputation based on your on-chain activity", + portal: "0xb86b3e16b6b960fd822849fd4b4861d73805879b", + schema: "0x105db2b6a3e9d79739bca3e1d9ddeec6bd68667cf1de8d4248020b91a9a80e46", + url: "https://trustgo.trustalabs.ai/etrusta/0x085ed975a8b6b860de3c2b871da60a3f9f48a5b8/lineaverax/h?chainId=324&s=EPR0QD9W52I8", + chainId: "0xe708", + }, + ], }, { - name: "Gitcoin Passport", - logo: Gitcoin, + name: "Passport XYZ", + logo: PassportXyz, keywords: ["reputation", "wallet score"], description: - "Gitcoin Passport is the best, easy-to-use identity and Sybil defense solution in web3. Top web3 projects protect what matters with Gitcoin Passport.", - CTALink: "https://passport.gitcoin.co/#/dashboard/verax", - CTATitle: "Get your passport", + "Passport XYZ enables web3 users to fairly participate in rewards, governance, and other community programs by helping partners better identify high-quality unique humans participating in their ecosystem.", + CTALink: "https://passport.gitcoin.co/#/verax/dashboard", + CTATitle: "Get your Passport", address: "0x96DB2c6D93A8a12089f7a6EdA5464e967308AdEd", + attestationDefinitions: [ + { + name: "Unique Humanity Score", + logo: PassportXyz, + description: + "Passport XYZ's Unique Humanity Score is the sum of different verifiable credentials, which together represents how unique and human the associated account is. Partners typically require users to have a score of 20+ to participate in various programs.", + portal: "0xcaa9e817f02486ce076560b77a86235ef91c5d5d", + schema: "0x01f031da36192c34057c764239eb77bb6ec8ebfb808f72a7bb172f37a5bec31f", + url: "https://passport.gitcoin.co/#/verax/dashboard", + chainId: "0xe708", + }, + ], }, { name: "PADO Labs", @@ -53,8 +85,20 @@ export const issuersData: IIssuer[] = [ description: "PADO is a zkAttestation protocol, dedicated to bringing Internet data into web3 smart contracts, expanding the capabilities of smart contracts, and enabling the monetization of personal data within data flows under privacy protection.", CTALink: "https://www.padolabs.org/events", - CTATitle: "Go to pado", + CTATitle: "Go to Pado", address: "0xDB736B13E2f522dBE18B2015d0291E4b193D8eF6", + attestationDefinitions: [ + { + name: "Binance KYC", + logo: Pado, + logoDark: PadoDark, + description: "PADO uses MPC-TLS and ZKP technology to attest you have a valid KYC on Binance", + portal: "0xc4b7dcba12866f6f8181b949ca443232c4e94334", + schema: "0x84fdf5748d9af166503472ff5deb0cd5f61f006169424805fd5554356ac6df10", + url: "https://padolabs.org/events", + chainId: "0xe708", + }, + ], }, { name: "zkPass", @@ -66,6 +110,38 @@ export const issuersData: IIssuer[] = [ CTALink: "https://verax.zkpass.org/verax", CTATitle: "Go To zkPass", address: "0x182085Ce8b0faDdc8503D9921dF6Af076281A6A9", + attestationDefinitions: [ + { + name: "OKX KYC", + logo: ZkPass, + logoDark: ZkPassDark, + description: "Prove you own OKX account with a valid KYC using zkPass' zk-TLS technology", + portal: "0x3b30d7c4e5aa3d7da11431af23e8d1f7d25bb0b8", + schema: "0x1299c489f12e79cf43672c791e9a962fb9ee2151784561f6ba2eb9ff6325a9a4", + url: "https://verax.zkpass.org/verax", + chainId: "0xe708", + }, + { + name: "Uber trips", + logo: ZkPass, + logoDark: ZkPassDark, + description: "Prove you own an account on Uber with at least 10 trips using zkPass' zk-TLS technology", + portal: "0x3b30d7c4e5aa3d7da11431af23e8d1f7d25bb0b8", + schema: "0xc0980771b02c57e851f0ecca619d593de82dc84b25db9c9273bbf5b1537276ae", + url: "https://verax.zkpass.org/verax", + chainId: "0xe708", + }, + { + name: "Coinbase KYC", + logo: ZkPass, + logoDark: ZkPassDark, + description: "Prove you own a Coinbase account with a valid KYC using zkPass' zk-TLS technology", + portal: "0x3b30d7c4e5aa3d7da11431af23e8d1f7d25bb0b8", + schema: "0x86e936ffddb895a13271ddb23cbf23b90ce44628b82de518dc0a6d117fed12db", + url: "https://verax.zkpass.org/verax", + chainId: "0xe708", + }, + ], }, { name: "Openid3", @@ -75,6 +151,18 @@ export const issuersData: IIssuer[] = [ CTALink: "https://auth.openid3.xyz", CTATitle: "Go to openid3", address: "0xdbCaf063873dC6be53c007Cf8f8447E303Cac8A3", + attestationDefinitions: [ + { + name: "Google account attestation", + logo: OpenId3, + description: + "Openid3 uses zero-knowledge proof to attest you own a Google account without revealing any information from this account", + portal: "0xce048492076b0130821866f6d05a0b621b1715c8", + schema: "0x912214269b9b891a0d7451974030ba13207d3bf78e515351609de9dd8a339686", + url: "https://app.orangeprotocol.io/campaigns/Linea/22", + chainId: "0xe708", + }, + ], }, { name: "Nomis", @@ -85,6 +173,18 @@ export const issuersData: IIssuer[] = [ CTALink: "https://nomis.cc/linea-voyage", CTATitle: "Go To Nomis", address: "0x8535156C75750d79ee0D9829c5D4Ae6f5D9DbCB5", + attestationDefinitions: [ + { + name: "Wallet Reputation score", + logo: Nomis, + description: + "The Wallet Reputation score, from 0 to 100, is computed based of your onchain footprints thanks to AI-powered mathematical modeling", + portal: "0x00df5a5eddb5e6a0d2ca38e193f82955a398b02a", + schema: "0x5d7cf069e8113d144e3c7cbec09f8f9b59d5f67a89269c957d5b0b7e6ca782b7", + url: "https://nomis.cc/linea-voyage", + chainId: "0xe708", + }, + ], }, { name: "Orange Protocol", @@ -95,6 +195,17 @@ export const issuersData: IIssuer[] = [ CTALink: "https://www.orangeprotocol.io/", CTATitle: "Go to orange", address: "0x3176383A7590D6B5c6F6268209f4c7FDeb7244Dc", + attestationDefinitions: [ + { + name: "Proof of personhood", + logo: Orange, + description: "Prove you are a real human using the BrightID network", + portal: "0x158c2eba6d050e9ef7b07250d2f4443a002f21c0", + schema: "0x0359a5c155f90c06ac75bcebd0d3cffaf13d0152f9a60d65b297baeb7476a024", + url: "https://app.orangeprotocol.io/campaigns/Linea/22", + chainId: "0xe708", + }, + ], }, { name: "0xScore", @@ -105,6 +216,18 @@ export const issuersData: IIssuer[] = [ CTALink: "https://0xscore.pro/linea-attestation", CTATitle: "Go To 0xScore", address: "0x04636DdD2feF7e9DB42a24821E489AD071749fEA", + attestationDefinitions: [ + { + name: "Reputation score", + logo: ZeroXScore, + description: + "0xScore provides a numerical reputation score for web3 addresses based on their on-chain behavior", + portal: "0xbdec68492d69a7ff1fb4c2abf5c28ade535dc88a", + schema: "0x6350a66dfb1aebcab88bb92c6fc179eb618472b7e0a95dee7ca5982a34610030", + url: "https://0xscore.io/linea-attestation", + chainId: "0xe708", + }, + ], }, { name: "Aspecta", @@ -114,7 +237,19 @@ export const issuersData: IIssuer[] = [ "Aspecta is an identity-centric hub for builder reputation, creation, and opportunity. We build an AI-powered identity for builders to demonstrate skills, experiences, and opinions based on GitHub and other data resources‘ insights.", CTALink: "https://aspecta.id/campaign/builders-voyage", CTATitle: "Go To Aspecta", - address: "0x36933bd4288648d95a8275e663003ae7efd2199d", + address: "0xd70ced3de8aafe99b5202ed4f6ba24c4029b39d8", + attestationDefinitions: [ + { + name: "Builder Achievements", + logo: Aspecta, + description: + "Claim your Builder credential. Receive badges based on your developer activity. Unlock token-gated experiences and more.", + portal: "0x36933bd4288648d95a8275e663003ae7efd2199d", + schema: "0xdb5ae35f38076cf76d5d1ca8e1e4f701ebd024773b19cd2b30601294943f3bff", + url: "https://aspecta.ai/builder-matrix/Linea-builder-launchpad", + chainId: "0xe708", + }, + ], }, { name: "Automata Network", @@ -123,8 +258,20 @@ export const issuersData: IIssuer[] = [ description: "Automata is a modular attestation layer that powers Proof of Machinehood. PoM is hardware-based attestation that provides verifiable claims about the identity, configuration and operational attributes of computing devices, which creates a viable framework for applications to be constructed upon an enduring bedrock of collective agency and data dignity.", CTALink: "https://pom.ata.network", - CTATitle: "Go To Automata", + CTATitle: "Attest your Machine", address: "0x95d06B395F04dc1bBD0CE9fcC501D7044ea25DAd", + attestationDefinitions: [ + { + name: "Proof of Machinehood", + logo: Automata, + description: + "“Proof of Machinehood” is an on-chain hardware attestation validating machine authenticity and capabilities without excessive computation or capital. Each machine, verified by the manufacturer, can sign and share its data on-chain.", + portal: "0xaf7452841e9a0851bead2d2b33f3494571a40d4c", + schema: "0xfcd7908635f4a15e4c4ae351f13f9aa393e56e67aca82e5ffd3cf5c463464ee7", + url: "https://pom.ata.network/", + chainId: "0xe708", + }, + ], }, { name: "Reclaim Protocol", @@ -135,6 +282,9 @@ export const issuersData: IIssuer[] = [ CTALink: "https://publish-credentials.reclaimprotocol.org/create-credential", CTATitle: "Go To Reclaim Protocol", address: "0xc15718EEC68DbCA02C4B4215B87beef46C3106d5", + attestationDefinitions: [ + // TODO: add Reclaim Protocol's information + ], }, { name: "RubyScore", @@ -145,6 +295,18 @@ export const issuersData: IIssuer[] = [ CTALink: "https://rubyscore.io/attestation", CTATitle: "Go To RubyScore", address: "0xb9cc0bb020cf55197c4c3d826ac87cadba51f272", + attestationDefinitions: [ + { + name: "RubyScore", + logo: RubyScore, + description: + "RubyScore uses on-chain metrics to determine the points assigned to each wallet, reflecting your activity", + portal: "0xb9cc0bb020cf55197c4c3d826ac87cadba51f272", + schema: "0xce6351ef35f71cd649b75be11a4d08a8420811e21db89085b27f56c9eeac1578", + url: "https://rubyscore.io/attestation", + chainId: "0xe708", + }, + ], }, { name: "Zeronym by Holonym", @@ -155,6 +317,19 @@ export const issuersData: IIssuer[] = [ CTALink: "https://holonym.id/", CTATitle: "Go To Holonym", address: "0xdca2e9ae8423d7b0f94d7f9fc09e698a45f3c851", + attestationDefinitions: [ + { + name: "Proof of personhood", + logo: Zeronym, + logoDark: ZeronymDark, + description: + "Holonym provides various methods to prove that you are a human while protecting your privacy with zero-knowledge proofs", + portal: "0x5631aecf3283922b6bf36d7485eb460f244bfac1", + schema: "0x1c14fd320660a59a50eb1f795116193a59c26f2463c0705b79d8cb97aa9f419b", + url: "", + chainId: "0xe708", + }, + ], }, { name: "Hapi", @@ -162,9 +337,21 @@ export const issuersData: IIssuer[] = [ keywords: ["Proof of Personhood", "Trust Score", "Security"], description: "HAPI ID is a digital identification of a user’s on-chain activity, created to simplify the interpretation of user’s action on the blockchain. Created for users, protocols, DApps, and businesses, HAPI ID serves as a one-stop unique solution against Sybils and for Users!", - CTALink: "https://hapi.one", - CTATitle: "Go To Hapi", + CTALink: "https://t.me/herewalletbot/app?startapp=web-score-hapi-mobi", + CTATitle: "Mint your score", address: "0x62773b3217e066a9a4ebd98db4360d89671453df", + attestationDefinitions: [ + { + name: "HAPI score", + logo: Hapi, + description: + "The HAPI Score is designed to assess the authenticity and activity of an on-chain account by analyzing various parameters related to its behavior and interactions.", + portal: "0xbc6897b3cd9be7411ecb88ba2840e3b1e8d431fb", + schema: "0xa913ce4f3de12a7b13304add3d7cd904794fc79c3d3f23b91a1914c2f19233e9", + url: "https://t.me/herewalletbot/app?startapp=web-score-hapi-mobi", + chainId: "0xe708", + }, + ], }, { name: "Okapi", @@ -176,17 +363,18 @@ export const issuersData: IIssuer[] = [ CTALink: "https://www.okapi.xyz", CTATitle: "Go To Okapi", address: "0xab3fa8a72eb66a128e8a84baa8c9578180806c6f", - }, - { - name: "Index Network", - logo: IndexNetwork, - logoDark: IndexNetworkDark, - keywords: ["Discoverability"], - description: - "Index Network is a discovery protocol that enables truly personalized and autonomous experiences across the web. By utilizing decentralized agents and semantic indexes, it facilitates the discovery of information in various fields, including science, journalism, e-commerce, and social interactions. Index Network transforms discovery into a protocol, making it a foundational layer for better information and user experience in web3.", - CTALink: "https://index.network/", - CTATitle: "Go To Index Network", - address: "0x0000000000000000000000000000000000000000", + attestationDefinitions: [ + { + name: "dApp review", + logo: Okapi, + logoDark: OkapiDark, + description: "Post your dApp reviews on-chain", + portal: "0xea7d7e414c17ce831ba7237b08d832f5e5327303", + schema: "0xec0f2d94ea5a78de8fcca98772fb8b4e36236bac1f081a6a8c745ed897c262b7", + url: "", + chainId: "0xe708", + }, + ], }, { name: "Privado ID", @@ -198,5 +386,18 @@ export const issuersData: IIssuer[] = [ CTALink: "https://www.privado.id/", CTATitle: "Go To Privado ID", address: "0x80203136fae3111b810106baa500231d4fd08fc6", + attestationDefinitions: [ + { + name: "Anima Proof of Uniqueness", + logo: PrivadoID, + logoDark: PrivadoID, + description: + "Prove that you are a unique human in two steps, by passing this multi-level credentialing leveraging biometrics face-scan system that ensures only unique humans can attain Level 2", + portal: "0x3486d714c6e6f7257fa7f0bb8396161150b9f100", + schema: "0x021fa993b2ac55b95340608478282821b89398de6fa14073b4d44a3564a8c79d", + url: "https://verax.privado.id/", + chainId: "0xe708", + }, + ], }, ]; diff --git a/explorer/src/pages/Home/interface.ts b/explorer/src/pages/Home/interface.ts index 04769790..c30cf835 100644 --- a/explorer/src/pages/Home/interface.ts +++ b/explorer/src/pages/Home/interface.ts @@ -9,4 +9,16 @@ export interface IIssuer { CTALink?: string; CTATitle: string; description: string; + attestationDefinitions: AttestationDefinition[]; +} + +export interface AttestationDefinition { + logo: React.FC>; + logoDark?: React.FC>; + name: string; + description: string; + portal: Address; + schema: Address; + url: string; + chainId: string; } diff --git a/explorer/src/pages/Issuer/components/Attestations/index.tsx b/explorer/src/pages/Issuer/components/Attestations/index.tsx new file mode 100644 index 00000000..ceabd742 --- /dev/null +++ b/explorer/src/pages/Issuer/components/Attestations/index.tsx @@ -0,0 +1,51 @@ +import { Loader } from "lucide-react"; +import { useLocation, useNavigate } from "react-router-dom"; +import useSWR from "swr"; + +import { SWRKeys } from "@/interfaces/swr/enum"; +import { useNetworkContext } from "@/providers/network-provider/context"; +import { APP_ROUTES, CHAIN_ID_ROUTE } from "@/routes/constants"; +import { formatNumber } from "@/utils/amountUtils"; + +import { IAttestationProps } from "./interface"; + +import "./styles.css"; + +export const Attestations: React.FC = ({ address }) => { + const { + sdk, + network: { network }, + } = useNetworkContext(); + const navigate = useNavigate(); + const location = useLocation(); + const { data: portals, isLoading } = useSWR(`${SWRKeys.GET_PORTALS_BY_ISSUER}/${address}`, () => + sdk.portal.findBy(undefined, undefined, { ownerAddress: address }), + ); + + const attestationCounter = portals ? portals.reduce((total, portal) => total + portal.attestationCounter, 0) : 0; + + const handleAttestationCounterClick = () => { + const whereClauseJSON = { + portal_in: portals?.map((portal) => portal.id), + }; + const whereClause = `?where=${encodeURIComponent(JSON.stringify(whereClauseJSON))}`; + navigate(APP_ROUTES.ATTESTATIONS.replace(CHAIN_ID_ROUTE, network) + whereClause, { + state: { from: location.pathname }, + }); + }; + + return ( +
+
+ Attestations +
+ {isLoading ? : formatNumber(attestationCounter)} +
+
+
+ ); +}; diff --git a/explorer/src/pages/Issuer/components/Attestations/interface.ts b/explorer/src/pages/Issuer/components/Attestations/interface.ts new file mode 100644 index 00000000..d2231420 --- /dev/null +++ b/explorer/src/pages/Issuer/components/Attestations/interface.ts @@ -0,0 +1,5 @@ +import { Address } from "viem"; + +export interface IAttestationProps { + address: Address; +} diff --git a/explorer/src/pages/Issuer/components/Attestations/styles.css b/explorer/src/pages/Issuer/components/Attestations/styles.css new file mode 100644 index 00000000..6a0d9e01 --- /dev/null +++ b/explorer/src/pages/Issuer/components/Attestations/styles.css @@ -0,0 +1,12 @@ +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.spinning-icon { + animation: spin 2s linear infinite; +} diff --git a/explorer/src/pages/Issuer/components/Banner/index.tsx b/explorer/src/pages/Issuer/components/Banner/index.tsx index f037de06..f16387d9 100644 --- a/explorer/src/pages/Issuer/components/Banner/index.tsx +++ b/explorer/src/pages/Issuer/components/Banner/index.tsx @@ -1,31 +1,37 @@ import { ArrowUpRight } from "lucide-react"; -import issuerBG from "@/assets/backgrounds/issuer-bg.jpeg"; import { Button } from "@/components/Buttons"; import { EButtonType } from "@/components/Buttons/enum"; +import { Chips } from "@/components/Chips"; import { IBannerProps } from "./interface"; -export const Banner: React.FC = ({ name, CTALink, CTATitle, logo }) => { +export const Banner: React.FC = ({ name, CTALink, CTATitle, logo, keywords }) => { const IssuerLogo = logo; - return ( -
- issuer background -
- -
- {name} +
+
+
+
+ +
+
{name}
+ {CTALink && ( +
+
+ {keywords.map((keyword) => ( + + ))}
- {CTALink && ( -
); }; diff --git a/explorer/src/pages/Issuer/components/Banner/interface.ts b/explorer/src/pages/Issuer/components/Banner/interface.ts index 08527188..df89a91c 100644 --- a/explorer/src/pages/Issuer/components/Banner/interface.ts +++ b/explorer/src/pages/Issuer/components/Banner/interface.ts @@ -3,4 +3,5 @@ export interface IBannerProps { logo: React.FC>; CTALink?: string; CTATitle: string; + keywords: string[]; } diff --git a/explorer/src/pages/Issuer/components/Description/index.tsx b/explorer/src/pages/Issuer/components/Description/index.tsx index 6c1a7bee..8409559a 100644 --- a/explorer/src/pages/Issuer/components/Description/index.tsx +++ b/explorer/src/pages/Issuer/components/Description/index.tsx @@ -1,15 +1,8 @@ -import { Chips } from "@/components/Chips"; - import { IDescriptionProps } from "./interface"; -export const Description: React.FC = ({ description, keywords }) => { +export const Description: React.FC = ({ description }) => { return ( -
-
- {keywords.map((keyword) => ( - - ))} -
+
{/* TODO: uncomment when data will be available */} {/*
@@ -21,7 +14,8 @@ export const Description: React.FC = ({ description, keywords
Schemas
*/} -
{description}
+
Info
+
{description}
); }; diff --git a/explorer/src/pages/Issuer/components/Description/interface.ts b/explorer/src/pages/Issuer/components/Description/interface.ts index 22b1b453..de070a7a 100644 --- a/explorer/src/pages/Issuer/components/Description/interface.ts +++ b/explorer/src/pages/Issuer/components/Description/interface.ts @@ -1,4 +1,3 @@ export interface IDescriptionProps { description: string; - keywords: string[]; } diff --git a/explorer/src/pages/Issuer/components/Portals/index.tsx b/explorer/src/pages/Issuer/components/Portals/index.tsx index 6977de55..a65ffe51 100644 --- a/explorer/src/pages/Issuer/components/Portals/index.tsx +++ b/explorer/src/pages/Issuer/components/Portals/index.tsx @@ -5,7 +5,6 @@ import useSWR from "swr"; import { Button } from "@/components/Buttons"; import { EButtonType } from "@/components/Buttons/enum"; -import { HelperIndicator } from "@/components/HelperIndicator"; import { SWRKeys } from "@/interfaces/swr/enum"; import { useNetworkContext } from "@/providers/network-provider/context"; import { CHAIN_ID_ROUTE, toPortalById } from "@/routes/constants"; @@ -27,55 +26,44 @@ export const Portals: React.FC = ({ address }) => { if (!portals) return null; return ( -
-
- {t("issuer.portals.title")} -
-
- {portals.map(({ ownerAddress, description, id, isRevocable, name }) => { - const additionalInfo = [ - { - title: t("common.id"), - value: id, - }, - { - title: t("issuer.portals.ownerAddress"), - value: ownerAddress, - }, - { - title: t("common.revocable"), - value: isRevocable ? t("common.yes") : t("common.no"), - }, - ]; - - return ( -
-
{name}
-
{description}
- {additionalInfo.map((info) => ( -
-
- {info.title} -
-
- {info.value} -
-
- ))} -
- ); - })} +
+
{t("issuer.portals.title")}
+
+ + + + + + + + + + {portals.map(({ id, name, description, attestationCounter }) => ( + + + + + + ))} + +
+ Name + + Description + + Attestations +
+ {description}{attestationCounter}
); diff --git a/explorer/src/pages/Issuer/components/Schemas/index.tsx b/explorer/src/pages/Issuer/components/Schemas/index.tsx new file mode 100644 index 00000000..118cc272 --- /dev/null +++ b/explorer/src/pages/Issuer/components/Schemas/index.tsx @@ -0,0 +1,75 @@ +import { t } from "i18next"; +import { ArrowUpRight, ChevronRight } from "lucide-react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useTernaryDarkMode } from "usehooks-ts"; + +import { Button } from "@/components/Buttons"; +import { EButtonType } from "@/components/Buttons/enum"; +import { AttestationDefinition } from "@/pages/Home/interface.ts"; +import { useNetworkContext } from "@/providers/network-provider/context"; +import { APP_ROUTES, CHAIN_ID_ROUTE } from "@/routes/constants"; + +import { ISchemasProps } from "./interface"; + +export const Schemas: React.FC = ({ issuerSchemas }) => { + const { + network: { network }, + } = useNetworkContext(); + const navigate = useNavigate(); + const location = useLocation(); + const { isDarkMode } = useTernaryDarkMode(); + + if (issuerSchemas === undefined || issuerSchemas.length === 0) return null; + + const handleSeeAttestationsClick = (portal: string, schema: string) => { + const whereClauseJSON = { + portal_: { id: portal }, + schema_: { id: schema }, + }; + const whereClause = `?where=${encodeURIComponent(JSON.stringify(whereClauseJSON))}`; + navigate(APP_ROUTES.ATTESTATIONS.replace(CHAIN_ID_ROUTE, network) + whereClause, { + state: { from: location.pathname }, + }); + }; + + const displayLogo = (attestationDefinition: AttestationDefinition) => { + const Logo: React.FC> = + isDarkMode && attestationDefinition.logoDark ? attestationDefinition.logoDark : attestationDefinition.logo; + return ; + }; + + return ( +
+ {issuerSchemas.map((issuerSchema) => ( +
+
+
+ {displayLogo(issuerSchema)} +
+ {issuerSchema.name} +
+
{issuerSchema.description}
+
+
+
+ ))} +
+ ); +}; diff --git a/explorer/src/pages/Issuer/components/Schemas/interface.ts b/explorer/src/pages/Issuer/components/Schemas/interface.ts new file mode 100644 index 00000000..d02a564c --- /dev/null +++ b/explorer/src/pages/Issuer/components/Schemas/interface.ts @@ -0,0 +1,6 @@ +import { AttestationDefinition } from "@/pages/Home/interface"; + +export interface ISchemasProps { + issuerSchemas?: AttestationDefinition[]; + CTALink?: string; +} diff --git a/explorer/src/pages/Issuer/index.tsx b/explorer/src/pages/Issuer/index.tsx index 5e9b95e8..27f5660c 100644 --- a/explorer/src/pages/Issuer/index.tsx +++ b/explorer/src/pages/Issuer/index.tsx @@ -1,30 +1,36 @@ import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { useTernaryDarkMode } from "usehooks-ts"; import { APP_ROUTES } from "@/routes/constants"; +import { Attestations } from "./components/Attestations"; import { Banner } from "./components/Banner"; import { Description } from "./components/Description"; -import { Portals } from "./components/Portals"; +import { Schemas } from "./components/Schemas"; import { issuersData } from "../Home/data"; export const Issuer: React.FC = () => { const { id } = useParams(); const navigate = useNavigate(); const location = useLocation(); + const { isDarkMode } = useTernaryDarkMode(); - const { name, description, CTALink, CTATitle, logo, keywords, address } = + const { name, description, CTALink, CTATitle, logo, logoDark, keywords, address, attestationDefinitions } = issuersData.find((issuer) => issuer.address === id) || {}; - if (!name || !description || !logo || !keywords || !address || !CTATitle) { + const IssuerLogo = isDarkMode && logoDark ? logoDark : logo; + + if (!name || !description || !IssuerLogo || !keywords || !address || !CTATitle) { navigate(APP_ROUTES.HOME, { state: { from: location.pathname } }); return null; } return ( -
- - - +
+ + + +
); }; diff --git a/explorer/src/utils/amountUtils.ts b/explorer/src/utils/amountUtils.ts index 68317e29..4804ca20 100644 --- a/explorer/src/utils/amountUtils.ts +++ b/explorer/src/utils/amountUtils.ts @@ -4,3 +4,13 @@ export const displayAmountWithComma = (str: string | number): string => .replace(/\B(?=(\d{3})+(?!\d))/g, ","); export const randomNumber = (min: number, max: number) => Math.floor(Math.random() * (max - min) + min); + +export const formatNumber = (num: number): string => { + if (num >= 1_000_000) { + return (num / 1_000_000).toFixed(1) + "M"; + } + if (num >= 1_000) { + return (num / 1_000).toFixed(1) + "K"; + } + return num.toString(); +};