diff --git a/websites/app/src/components/Dashboard/RecentActivity.tsx b/websites/app/src/components/Dashboard/RecentActivity.tsx index 0743a5a..21e90f2 100644 --- a/websites/app/src/components/Dashboard/RecentActivity.tsx +++ b/websites/app/src/components/Dashboard/RecentActivity.tsx @@ -1,42 +1,42 @@ -import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import styled, { css } from 'styled-components'; -import { landscapeStyle } from 'styles/landscapeStyle'; -import { useNavigate } from 'react-router-dom'; -import { useItemsQuery } from 'hooks/queries/useItemsQuery'; -import { revRegistryMap } from 'utils/items'; -import { shortenAddress } from 'utils/shortenAddress'; -import HourglassIcon from 'svgs/icons/hourglass.svg'; +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react' +import styled, { css } from 'styled-components' +import { landscapeStyle } from 'styles/landscapeStyle' +import { useNavigate } from 'react-router-dom' +import { useItemsQuery } from 'hooks/queries/useItemsQuery' +import { revRegistryMap } from 'utils/items' +import { shortenAddress } from 'utils/shortenAddress' +import HourglassIcon from 'svgs/icons/hourglass.svg' import useHumanizedCountdown, { useChallengeRemainingTime, useChallengePeriodDuration, -} from 'hooks/countdown'; - -import ArbitrumIcon from 'svgs/chains/arbitrum.svg'; -import AvalancheIcon from 'svgs/chains/avalanche.svg'; -import BaseIcon from 'svgs/chains/base.svg'; -import BnbIcon from 'svgs/chains/bnb.svg'; -import CeloIcon from 'svgs/chains/celo.svg'; -import EthereumIcon from 'svgs/chains/ethereum.svg'; -import FantomIcon from 'svgs/chains/fantom.svg'; -import GnosisIcon from 'svgs/chains/gnosis.svg'; -import OptimismIcon from 'svgs/chains/optimism.svg'; -import PolygonIcon from 'svgs/chains/polygon.svg'; -import ScrollIcon from 'svgs/chains/scroll.svg'; -import SolanaIcon from 'svgs/chains/solana.svg'; -import ZkSyncIcon from 'svgs/chains/zksync.svg'; +} from 'hooks/countdown' + +import ArbitrumIcon from 'svgs/chains/arbitrum.svg' +import AvalancheIcon from 'svgs/chains/avalanche.svg' +import BaseIcon from 'svgs/chains/base.svg' +import BnbIcon from 'svgs/chains/bnb.svg' +import CeloIcon from 'svgs/chains/celo.svg' +import EthereumIcon from 'svgs/chains/ethereum.svg' +import FantomIcon from 'svgs/chains/fantom.svg' +import GnosisIcon from 'svgs/chains/gnosis.svg' +import OptimismIcon from 'svgs/chains/optimism.svg' +import PolygonIcon from 'svgs/chains/polygon.svg' +import ScrollIcon from 'svgs/chains/scroll.svg' +import SolanaIcon from 'svgs/chains/solana.svg' +import ZkSyncIcon from 'svgs/chains/zksync.svg' // Constants - must be defined before styled components const DOT_COLORS = { active: '#C5ABFF', inactive: '#2A2A2A', -} as const; +} as const const ACTIVITY_COLORS = { Challenged: '#E87B35', Submitted: '#60A5FA', Included: '#65DC7F', default: '#9CA3AF', -} as const; +} as const const chainIconMap: Record> = { '42161': ArbitrumIcon, @@ -53,33 +53,33 @@ const chainIconMap: Record> = { '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ': SolanaIcon, '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': SolanaIcon, '324': ZkSyncIcon, -}; +} -const CAROUSEL_INTERVAL = 7500; -const ITEMS_PER_GROUP = 3; -const MAX_ITEMS = 9; +const CAROUSEL_INTERVAL = 7500 +const ITEMS_PER_GROUP = 3 +const MAX_ITEMS = 9 -const Container = styled.div``; +const Container = styled.div`` const Header = styled.div` display: flex; align-items: center; gap: 12px; margin-bottom: 24px; -`; +` const Title = styled.h3` font-size: 18px; font-weight: 600; color: ${({ theme }) => theme.primaryText}; margin: 0; -`; +` const ActivityList = styled.div` display: flex; flex-direction: column; gap: 8px; -`; +` const ActivityCard = styled.div` display: flex; @@ -88,28 +88,32 @@ const ActivityCard = styled.div` margin-bottom: 8px; border-radius: 12px; border: 1px solid ${({ theme }) => theme.lightGrey}; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(153, 153, 153, 0.08) 100%); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(153, 153, 153, 0.08) 100% + ); cursor: pointer; transition: all 0.2s ease; gap: 8px; - + &:hover { transform: translateY(-2px); box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); border-color: ${({ theme }) => theme.primary}; } - + &:last-child { margin-bottom: 0; } -`; +` const FirstLine = styled.div` display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; -`; +` const LeftSection = styled.div` display: flex; @@ -117,21 +121,21 @@ const LeftSection = styled.div` gap: 2px; flex: 1; min-width: 0; - + ${landscapeStyle( () => css` flex-direction: row; align-items: center; gap: 8px; - ` + `, )} -`; +` const SecondLine = styled.div` display: flex; justify-content: space-between; align-items: center; -`; +` const ActivityName = styled.h3` font-size: 16px; @@ -141,17 +145,16 @@ const ActivityName = styled.h3` line-height: 1.3; word-break: break-word; overflow-wrap: break-word; - + ${landscapeStyle( () => css` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; - ` + `, )} -`; - +` const ActivityType = styled.span` font-size: 12px; @@ -160,7 +163,7 @@ const ActivityType = styled.span` line-height: 1.2; display: flex; align-items: center; -`; +` const RegistryType = styled.span` color: ${({ theme }) => theme.secondaryText}; @@ -168,14 +171,14 @@ const RegistryType = styled.span` font-weight: 400; flex-shrink: 0; margin-top: 2px; - + ${landscapeStyle( () => css` font-size: 14px; margin-top: 0; - ` + `, )} -`; +` const TimeInfo = styled.div` display: flex; @@ -183,13 +186,13 @@ const TimeInfo = styled.div` gap: 6px; color: ${({ theme }) => theme.secondaryText}; font-size: 13px; - + svg { width: 14px; height: 14px; fill: ${({ theme }) => theme.secondaryText}; } -`; +` const RightSection = styled.div` display: flex; @@ -199,34 +202,36 @@ const RightSection = styled.div` flex-wrap: wrap; justify-content: flex-end; margin-top: 4px; - + ${landscapeStyle( () => css` gap: 8px; margin-top: 0; - ` + `, )} -`; +` const ChainIcon = styled.div` width: 16px; height: 16px; flex-shrink: 0; margin-bottom: 8px; - + svg { width: 100%; height: 100%; } -`; +` const StatusIcon = styled.div<{ status: string }>` width: 8px; height: 8px; border-radius: 50%; - background: ${({ status }) => ACTIVITY_COLORS[status as keyof typeof ACTIVITY_COLORS] || ACTIVITY_COLORS.default}; + background: ${({ status }) => + ACTIVITY_COLORS[status as keyof typeof ACTIVITY_COLORS] || + ACTIVITY_COLORS.default}; flex-shrink: 0; -`; +` const ViewButton = styled.div` display: flex; @@ -235,215 +240,247 @@ const ViewButton = styled.div` color: ${({ theme }) => theme.primaryText}; font-size: 14px; font-weight: 500; - + &::after { content: '→'; font-size: 16px; } -`; +` const DotsContainer = styled.div` display: flex; justify-content: center; gap: 8px; margin-top: 24px; -`; +` const Dot = styled.div<{ active: boolean }>` width: 8px; height: 8px; border-radius: 50%; - background: ${({ active }) => active ? DOT_COLORS.active : DOT_COLORS.inactive}; + background: ${({ active }) => + active ? DOT_COLORS.active : DOT_COLORS.inactive}; cursor: pointer; transition: all 0.2s ease; - + &:hover { background: ${DOT_COLORS.active}; } -`; +` const LoadingCard = styled.div` padding: 16px; margin-bottom: 12px; border-radius: 8px; border: 1px solid ${({ theme }) => theme.lightGrey}; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(153, 153, 153, 0.08) 100%); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(153, 153, 153, 0.08) 100% + ); animation: pulse 2s infinite; - + @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } } -`; +` const EmptyState = styled.div` padding: 20px; text-align: center; color: #666; -`; +` interface ActivityItem { - id: string; - itemID: string; - registryAddress: string; - disputed: boolean; - status: 'Registered' | 'RegistrationRequested' | 'ClearingRequested' | 'Absent'; - metadata?: { - key0: string; - key1: string; - key2: string; - key3: string; - key4: string; - props: Array<{ - label: string; - value: string; - }>; - } | null; + id: string + itemID: string + registryAddress: string + disputed: boolean + status: + | 'Registered' + | 'RegistrationRequested' + | 'ClearingRequested' + | 'Absent' + key0: string + key1: string + key2: string + key3: string + key4: string + props: Array<{ + label: string + value: string + }> requests?: Array<{ - submissionTime: string; - requester: string; - }>; + submissionTime: string + requester: string + }> } -type ActivityStatus = 'Challenged' | 'Submitted' | 'Included' | 'Removing' | 'Removed'; +type ActivityStatus = + | 'Challenged' + | 'Submitted' + | 'Included' + | 'Removing' + | 'Removed' const getProp = (item: ActivityItem, label: string): string => - item?.metadata?.props?.find((p) => p.label === label)?.value ?? ""; + item?.props?.find((p) => p.label === label)?.value ?? '' const getChainId = (item: ActivityItem): string | undefined => { - const key0 = item?.metadata?.key0; - if (!key0) return undefined; - - const parts = key0.split(':'); - return parts[1]; // Extract chain ID from format like "eip155:1:0x..." -}; + const key0 = item?.key0 + if (!key0) return undefined + + const parts = key0.split(':') + return parts[1] // Extract chain ID from format like "eip155:1:0x..." +} const getChainIcon = (chainId: string | undefined) => { - if (!chainId) return null; - return chainIconMap[chainId] || null; -}; + if (!chainId) return null + return chainIconMap[chainId] || null +} export const RecentActivity: React.FC = () => { - const [currentGroup, setCurrentGroup] = useState(0); - const intervalRef = useRef(null); - const navigate = useNavigate(); + const [currentGroup, setCurrentGroup] = useState(0) + const intervalRef = useRef(null) + const navigate = useNavigate() const searchParams = useMemo(() => { - const params = new URLSearchParams(); + const params = new URLSearchParams() // Registries to fetch from - ['Tokens', 'CDN', 'Single_Tags', 'Tags_Queries'].forEach(registry => { - params.append('registry', registry); - }); + ;['Tokens', 'CDN', 'Single_Tags', 'Tags_Queries'].forEach((registry) => { + params.append('registry', registry) + }) // Status filters - ['Registered', 'RegistrationRequested', 'ClearingRequested'].forEach(status => { - params.append('status', status); - }); + ;['Registered', 'RegistrationRequested', 'ClearingRequested'].forEach( + (status) => { + params.append('status', status) + }, + ) // Include both disputed and non-disputed items - ['true', 'false'].forEach(disputed => { - params.append('disputed', disputed); - }); - params.append('orderDirection', 'desc'); - params.append('page', '1'); - return params; - }, []); + ;['true', 'false'].forEach((disputed) => { + params.append('disputed', disputed) + }) + params.append('orderDirection', 'desc') + params.append('page', '1') + return params + }, []) const { data: items = [], isLoading } = useItemsQuery({ searchParams, chainFilters: [], enabled: true, - }); + }) const itemGroups = useMemo(() => { - if (!items.length) return []; - - const groups: ActivityItem[][] = []; - const maxItems = Math.min(items.length, MAX_ITEMS); - + if (!items.length) return [] + + const groups: ActivityItem[][] = [] + const maxItems = Math.min(items.length, MAX_ITEMS) + for (let i = 0; i < maxItems; i += ITEMS_PER_GROUP) { - groups.push(items.slice(i, i + ITEMS_PER_GROUP)); + groups.push(items.slice(i, i + ITEMS_PER_GROUP)) } - return groups; - }, [items]); + return groups + }, [items]) useEffect(() => { - if (itemGroups.length <= 1) return; - + if (itemGroups.length <= 1) return + const interval = setInterval(() => { - setCurrentGroup((prev) => (prev + 1) % itemGroups.length); - }, CAROUSEL_INTERVAL); - - intervalRef.current = interval; - - return () => clearInterval(interval); - }, [itemGroups.length]); - - const handleCardClick = useCallback((item: ActivityItem) => { - const registryName = revRegistryMap[item.registryAddress] ?? "Unknown"; - const params = new URLSearchParams(); - - params.set("registry", registryName); - ['Registered', 'RegistrationRequested', 'ClearingRequested', 'Absent'].forEach(status => { - params.set("status", status); - }); - ['true', 'false'].forEach(disputed => { - params.set("disputed", disputed); - }); - params.set("page", "1"); - params.set("orderDirection", "desc"); - - navigate(`/item/${item.id}?${params.toString()}`); - }, [navigate]); - - const handleDotClick = useCallback((index: number) => { - setCurrentGroup(index); - - // Reset the timer by clearing current interval and starting a new one - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - - if (itemGroups.length > 1) { - const newInterval = setInterval(() => { - setCurrentGroup((prev) => (prev + 1) % itemGroups.length); - }, CAROUSEL_INTERVAL); - intervalRef.current = newInterval; - } - }, [itemGroups.length]); + setCurrentGroup((prev) => (prev + 1) % itemGroups.length) + }, CAROUSEL_INTERVAL) + + intervalRef.current = interval + + return () => clearInterval(interval) + }, [itemGroups.length]) + + const handleCardClick = useCallback( + (item: ActivityItem) => { + const registryName = revRegistryMap[item.registryAddress] ?? 'Unknown' + const params = new URLSearchParams() + + params.set('registry', registryName) + ;[ + 'Registered', + 'RegistrationRequested', + 'ClearingRequested', + 'Absent', + ].forEach((status) => { + params.set('status', status) + }) + ;['true', 'false'].forEach((disputed) => { + params.set('disputed', disputed) + }) + params.set('page', '1') + params.set('orderDirection', 'desc') + + navigate(`/item/${item.id}?${params.toString()}`) + }, + [navigate], + ) + + const handleDotClick = useCallback( + (index: number) => { + setCurrentGroup(index) + + // Reset the timer by clearing current interval and starting a new one + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + + if (itemGroups.length > 1) { + const newInterval = setInterval(() => { + setCurrentGroup((prev) => (prev + 1) % itemGroups.length) + }, CAROUSEL_INTERVAL) + intervalRef.current = newInterval + } + }, + [itemGroups.length], + ) const getDisplayName = useCallback((item: ActivityItem): string => { - const propNames = ["Name", "Domain name", "Public Name Tag", "Description"]; - + const propNames = ['Name', 'Domain name', 'Public Name Tag', 'Description'] + for (const propName of propNames) { - const value = getProp(item, propName); - if (value) return value; + const value = getProp(item, propName) + if (value) return value } - - return shortenAddress(item.itemID); - }, []); + + return shortenAddress(item.itemID) + }, []) const getActivityType = useCallback((item: ActivityItem): ActivityStatus => { - if (item.disputed) return 'Challenged'; - if (item.status === 'RegistrationRequested') return 'Submitted'; - if (item.status === 'Registered') return 'Included'; - if (item.status === 'ClearingRequested') return 'Removing'; - if (item.status === 'Absent') return 'Removed'; - return 'Submitted'; - }, []); + if (item.disputed) return 'Challenged' + if (item.status === 'RegistrationRequested') return 'Submitted' + if (item.status === 'Registered') return 'Included' + if (item.status === 'ClearingRequested') return 'Removing' + if (item.status === 'Absent') return 'Removed' + return 'Submitted' + }, []) const ActivityCardItem: React.FC<{ item: ActivityItem }> = ({ item }) => { - const registryName = revRegistryMap[item.registryAddress] ?? "Unknown"; - const displayName = getDisplayName(item); - const activityType = getActivityType(item); - - const challengePeriodDuration = useChallengePeriodDuration(item.registryAddress); + const registryName = revRegistryMap[item.registryAddress] ?? 'Unknown' + const displayName = getDisplayName(item) + const activityType = getActivityType(item) + + const challengePeriodDuration = useChallengePeriodDuration( + item.registryAddress, + ) const endsAtSeconds = useChallengeRemainingTime( item.requests?.[0]?.submissionTime, item.disputed, - challengePeriodDuration - ); - const endsIn = useHumanizedCountdown(endsAtSeconds, 2); - const showEndsIn = Boolean(endsIn) && item.status !== "Registered"; - + challengePeriodDuration, + ) + const endsIn = useHumanizedCountdown(endsAtSeconds, 2) + const showEndsIn = Boolean(endsIn) && item.status !== 'Registered' + return ( handleCardClick(item)}> @@ -453,17 +490,15 @@ export const RecentActivity: React.FC = () => { - - {activityType} - + {activityType} {(() => { - const chainId = getChainId(item); - const ChainIconComponent = getChainIcon(chainId); + const chainId = getChainId(item) + const ChainIconComponent = getChainIcon(chainId) return ChainIconComponent ? ( - ) : null; + ) : null })()} @@ -471,19 +506,16 @@ export const RecentActivity: React.FC = () => { - {showEndsIn + {showEndsIn ? `Will be included in: ${endsIn}` - : 'Already included' - } + : 'Already included'} - - View - + View - ); - }; + ) + } if (isLoading) { return ( @@ -497,7 +529,7 @@ export const RecentActivity: React.FC = () => { ))} - ); + ) } if (!itemGroups.length) { @@ -507,12 +539,10 @@ export const RecentActivity: React.FC = () => { Recent Activity - - No recent activity found - + No recent activity found - ); + ) } return ( @@ -537,5 +567,5 @@ export const RecentActivity: React.FC = () => { )} - ); -}; \ No newline at end of file + ) +} diff --git a/websites/app/src/consts/index.tsx b/websites/app/src/consts/index.tsx index 275d173..c6a4018 100644 --- a/websites/app/src/consts/index.tsx +++ b/websites/app/src/consts/index.tsx @@ -1,9 +1,14 @@ export const EMAIL_REGEX = - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -export const TELEGRAM_REGEX = /^@\w{5,32}$/; -export const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; -export const ETH_SIGNATURE_REGEX = /^0x[a-fA-F0-9]{130}$/; + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ +export const TELEGRAM_REGEX = /^@\w{5,32}$/ +export const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/ +export const ETH_SIGNATURE_REGEX = /^0x[a-fA-F0-9]{130}$/ -export const DAPPLOOKER_API_KEY = import.meta.env.REACT_APP_DAPPLOOKER_API_KEY || ""; -export const SUBGRAPH_GNOSIS_ENDPOINT = import.meta.env.REACT_APP_SUBGRAPH_GNOSIS_ENDPOINT || "https://api.studio.thegraph.com/query/108432/gtcr-subgraph-gnosis/version/latest"; -export const SUBGRAPH_KLEROS_DISPLAY_GNOSIS_ENDPOINT = import.meta.env.REACT_APP_SUBGRAPH_KLEROS_DISPLAY_GNOSIS_ENDPOINT || "https://api.studio.thegraph.com/query/61738/kleros-display-gnosis/version/latest"; +export const DAPPLOOKER_API_KEY = + import.meta.env.REACT_APP_DAPPLOOKER_API_KEY || '' +export const SUBGRAPH_GNOSIS_ENDPOINT = + import.meta.env.REACT_APP_SUBGRAPH_GNOSIS_ENDPOINT || + 'https://indexer.hyperindex.xyz/1a2f51c/v1/graphql' +export const SUBGRAPH_KLEROS_DISPLAY_GNOSIS_ENDPOINT = + import.meta.env.REACT_APP_SUBGRAPH_KLEROS_DISPLAY_GNOSIS_ENDPOINT || + 'https://api.studio.thegraph.com/query/61738/kleros-display-gnosis/version/latest' diff --git a/websites/app/src/hooks/queries/useExportItems.ts b/websites/app/src/hooks/queries/useExportItems.ts index 8d2b075..1bc9210 100644 --- a/websites/app/src/hooks/queries/useExportItems.ts +++ b/websites/app/src/hooks/queries/useExportItems.ts @@ -1,17 +1,17 @@ -import { gql, request } from 'graphql-request'; -import { GraphItem } from 'utils/items'; -import { useQuery } from '@tanstack/react-query'; -import { SUBGRAPH_GNOSIS_ENDPOINT } from 'consts/index'; -import { chains, getNamespaceForChainId } from 'utils/chains'; +import { gql, request } from 'graphql-request' +import { GraphItem } from 'utils/items' +import { useQuery } from '@tanstack/react-query' +import { SUBGRAPH_GNOSIS_ENDPOINT } from 'consts/index' +import { chains, getNamespaceForChainId } from 'utils/chains' export interface ExportFilters { - registryId?: string; - status?: string[]; - disputed?: boolean[]; - fromDate?: string; - toDate?: string; - network?: string[]; - text?: string; + registryId?: string + status?: string[] + disputed?: boolean[] + fromDate?: string + toDate?: string + network?: string[] + text?: string } export const useExportItems = (filters: ExportFilters) => { @@ -19,10 +19,10 @@ export const useExportItems = (filters: ExportFilters) => { queryKey: ['exportItems', filters], enabled: false, // Only fetch when export button is clicked queryFn: async () => { - let allData: GraphItem[] = []; - const first = 1000; - let skip = 0; - let keepFetching = true; + let allData: GraphItem[] = [] + const first = 1000 + let skip = 0 + let keepFetching = true const { registryId, @@ -31,102 +31,113 @@ export const useExportItems = (filters: ExportFilters) => { fromDate, toDate, network = [], - text = '' - } = filters; + text = '', + } = filters if (!registryId) { - throw new Error('Registry ID is required for export'); + throw new Error('Registry ID is required for export') } - const isTagsQueriesRegistry = registryId === '0xae6aaed5434244be3699c56e7ebc828194f26dc3'; + const isTagsQueriesRegistry = + registryId === '0xae6aaed5434244be3699c56e7ebc828194f26dc3' // Build network filter - const selectedChainIds = network.filter((id) => id !== 'unknown'); - const includeUnknown = network.includes('unknown'); - const definedChainIds = chains.map((c) => c.id); - const knownPrefixes = [...new Set(chains.map((chain) => { - if (chain.namespace === 'solana') { - return 'solana:'; - } - return `${chain.namespace}:${chain.id}:`; - }))]; + const selectedChainIds = network.filter((id) => id !== 'unknown') + const includeUnknown = network.includes('unknown') + const definedChainIds = chains.map((c) => c.id) + const knownPrefixes = [ + ...new Set( + chains.map((chain) => { + if (chain.namespace === 'solana') { + return 'solana:' + } + return `${chain.namespace}:${chain.id}:` + }), + ), + ] - let networkQueryObject = ''; + let networkQueryObject = '' if (isTagsQueriesRegistry && network.length > 0) { const conditions = selectedChainIds.map( (chainId) => - `{or: [{metadata_: {key2: "${chainId}"}}, {metadata_: {key1: "${chainId}"}}]}` - ); + `{ _or: [{ key2: { _eq: "${chainId}"}}, { key1: { _eq: "${chainId}"}}]}`, + ) if (includeUnknown) { conditions.push( - `{and: [{metadata_: {key1_not_in: $definedChainIds}}, {metadata_: {key2_not_in: $definedChainIds}}]}` - ); + `{ _and: [{ key1: { _nin: $definedChainIds}}, { key2: { _nin: $definedChainIds}}]}`, + ) } - networkQueryObject = conditions.length > 0 ? `{or: [${conditions.join(',')}]}` : '{}'; + networkQueryObject = + conditions.length > 0 ? `{_or: [${conditions.join(',')}]}` : '{}' } else if (network.length > 0) { const conditions = selectedChainIds.map((chainId) => { - const namespace = getNamespaceForChainId(chainId); + const namespace = getNamespaceForChainId(chainId) if (namespace === 'solana') { - return `{metadata_: {key0_starts_with_nocase: "solana:"}}`; + return `{key0: { _ilike: "solana:%"}}` } - return `{metadata_: {key0_starts_with_nocase: "${namespace}:${chainId}:"}}`; - }); - networkQueryObject = conditions.length > 0 ? `{or: [${conditions.join(',')}]}` : '{}'; + return `{key0: {_ilike: "${namespace}:${chainId}:%"}}` + }) + networkQueryObject = + conditions.length > 0 ? `{_or: [${conditions.join(',')}]}` : '{}' } // Build text filter const textFilterObject = text - ? `{or: [ - {metadata_: {key0_contains_nocase: $text}}, - {metadata_: {key1_contains_nocase: $text}}, - {metadata_: {key2_contains_nocase: $text}}, - {metadata_: {key3_contains_nocase: $text}}, - {metadata_: {key4_contains_nocase: $text}} - ]}` - : ''; + ? `{_or: [ + {key0: {_ilike: $text}}, + {key1: {_ilike: $text}}, + {key2: {_ilike: $text}}, + {key3: {_ilike: $text}}, + {key4: {_ilike: $text}} + ]}` + : '' // Build date filter - let dateFilterObject = ''; + let dateFilterObject = '' if (fromDate || toDate) { - const conditions = []; + const conditions: string[] = [] if (fromDate) { - const fromTimestamp = Math.floor(new Date(fromDate).getTime() / 1000); - conditions.push(`{latestRequestSubmissionTime_gte: "${fromTimestamp}"}`); + const fromTimestamp = Math.floor(new Date(fromDate).getTime() / 1000) + conditions.push( + `{latestRequestSubmissionTime: { _gte: "${fromTimestamp}"}}`, + ) } if (toDate) { - const toTimestamp = Math.floor(new Date(toDate).getTime() / 1000); - conditions.push(`{latestRequestSubmissionTime_lte: "${toTimestamp}"}`); + const toTimestamp = Math.floor(new Date(toDate).getTime() / 1000) + conditions.push( + `{latestRequestSubmissionTime: {_lte: "${toTimestamp}"}}`, + ) } - dateFilterObject = conditions.length > 0 ? `{and: [${conditions.join(',')}]}` : ''; + dateFilterObject = + conditions.length > 0 ? `{_and: [${conditions.join(',')}]}` : '' } // Build the complete where clause const whereConditions = [ - `{registry: "${registryId}"}`, - `{status_in: $status}`, - `{disputed_in: $disputed}`, + `{registry_id: {_eq: "${registryId}"}}`, + `{status: {_in: $status}}`, + `{disputed: {_in: $disputed}}`, networkQueryObject && `${networkQueryObject}`, textFilterObject && `${textFilterObject}`, - dateFilterObject && `${dateFilterObject}` - ].filter(Boolean) as string[]; + dateFilterObject && `${dateFilterObject}`, + ].filter(Boolean) as string[] const query = gql` query ( - $status: [String!]! + $status: [status!]! $disputed: [Boolean!]! - $text: String! + $text: String $skip: Int! $first: Int! ${includeUnknown && isTagsQueriesRegistry ? '$definedChainIds: [String!]!' : ''} ) { - litems( + litems: LItem( where: { - and: [${whereConditions.join(',')}] + _and: [${whereConditions.join(',')}] } - skip: $skip - first: $first - orderBy: "latestRequestSubmissionTime" - orderDirection: desc + offset: $skip + limit: $first + order_by: {latestRequestSubmissionTime: desc} ) { id latestRequestSubmissionTime @@ -135,21 +146,19 @@ export const useExportItems = (filters: ExportFilters) => { status disputed data - metadata { - key0 - key1 - key2 - key3 - key4 - props { - value - type - label - description - isIdentifier - } + key0 + key1 + key2 + key3 + key4 + props { + value + type: itemType + label + description + isIdentifier } - requests(first: 1, orderBy: submissionTime, orderDirection: desc) { + requests(limit: 1, order_by: {submissionTime: desc}) { disputed disputeID submissionTime @@ -158,7 +167,7 @@ export const useExportItems = (filters: ExportFilters) => { challenger resolutionTime deposit - rounds(first: 1, orderBy: creationTime, orderDirection: desc) { + rounds(limit: 1, order_by: {creationTime : desc}) { appealPeriodStart appealPeriodEnd ruling @@ -170,65 +179,77 @@ export const useExportItems = (filters: ExportFilters) => { } } } - `; + ` try { while (keepFetching) { const variables: any = { status, disputed, - text, skip, first, - }; + } + + if (text) { + variables.text = `%${text}%` + } + if (includeUnknown && isTagsQueriesRegistry) { - variables.definedChainIds = definedChainIds; + variables.definedChainIds = definedChainIds } const result = (await request({ url: SUBGRAPH_GNOSIS_ENDPOINT, document: query, variables, - })) as any; + })) as any - let items = result.litems; + let items = result.litems // Client-side filtering for non-Tags_Queries registries if (!isTagsQueriesRegistry && network.length > 0) { const selectedPrefixes = selectedChainIds.map((chainId) => { - const namespace = getNamespaceForChainId(chainId); + const namespace = getNamespaceForChainId(chainId) if (namespace === 'solana') { - return 'solana:'; + return 'solana:' } - return `${namespace}:${chainId}:`; - }); + return `${namespace}:${chainId}:` + }) items = items.filter((item: GraphItem) => { - const key0 = item.metadata?.key0?.toLowerCase() || ''; - const matchesSelectedChain = selectedPrefixes.length > 0 - ? selectedPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())) - : false; - - const isUnknownChain = !knownPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())); - - return (selectedPrefixes.length > 0 && matchesSelectedChain) || (includeUnknown && isUnknownChain); - }); + const key0 = item?.key0?.toLowerCase() || '' + const matchesSelectedChain = + selectedPrefixes.length > 0 + ? selectedPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) + : false + + const isUnknownChain = !knownPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) + + return ( + (selectedPrefixes.length > 0 && matchesSelectedChain) || + (includeUnknown && isUnknownChain) + ) + }) } - allData = allData.concat(items); + allData = allData.concat(items) if (items.length < first) { - keepFetching = false; + keepFetching = false } - skip += first; + skip += first } } catch (error) { - console.error('Error fetching export data:', error); - throw error; + console.error('Error fetching export data:', error) + throw error } - return allData; + return allData }, - }); -}; \ No newline at end of file + }) +} diff --git a/websites/app/src/hooks/queries/useItemCountsQuery.ts b/websites/app/src/hooks/queries/useItemCountsQuery.ts index 5c628ec..34208be 100644 --- a/websites/app/src/hooks/queries/useItemCountsQuery.ts +++ b/websites/app/src/hooks/queries/useItemCountsQuery.ts @@ -1,14 +1,17 @@ -import { useQuery } from '@tanstack/react-query'; -import { gql } from 'graphql-request'; -import { queryKeys, REFETCH_INTERVAL, STALE_TIME } from './consts'; -import { useGraphqlBatcher } from './useGraphqlBatcher'; -import { ItemCounts } from '../../utils/itemCounts'; -import { registryMap } from 'utils/items'; -import { fetchRegistryDeposits, DepositParams } from '../../utils/fetchRegistryDeposits'; +import { useQuery } from '@tanstack/react-query' +import { gql } from 'graphql-request' +import { queryKeys, REFETCH_INTERVAL, STALE_TIME } from './consts' +import { useGraphqlBatcher } from './useGraphqlBatcher' +import { ItemCounts } from '../../utils/itemCounts' +import { registryMap } from 'utils/items' +import { + fetchRegistryDeposits, + DepositParams, +} from '../../utils/fetchRegistryDeposits' const FETCH_ITEM_COUNTS_QUERY = gql` query FetchItemCounts { - Single_Tags: lregistry(id: "${registryMap.Single_Tags}") { + Single_Tags: LRegistry_by_pk(id: "${registryMap.Single_Tags}") { id numberOfAbsent numberOfRegistered @@ -17,10 +20,10 @@ const FETCH_ITEM_COUNTS_QUERY = gql` numberOfRegistrationRequested numberOfChallengedRegistrations registrationMetaEvidence { - URI + URI: uri } } - Tags_Queries: lregistry(id: "${registryMap.Tags_Queries}") { + Tags_Queries: LRegistry_by_pk(id: "${registryMap.Tags_Queries}") { id numberOfAbsent numberOfRegistered @@ -29,10 +32,10 @@ const FETCH_ITEM_COUNTS_QUERY = gql` numberOfRegistrationRequested numberOfChallengedRegistrations registrationMetaEvidence { - URI + URI: uri } } - CDN: lregistry(id: "${registryMap.CDN}") { + CDN: LRegistry_by_pk(id: "${registryMap.CDN}") { id numberOfAbsent numberOfRegistered @@ -41,10 +44,10 @@ const FETCH_ITEM_COUNTS_QUERY = gql` numberOfRegistrationRequested numberOfChallengedRegistrations registrationMetaEvidence { - URI + URI: uri } } - Tokens: lregistry(id: "${registryMap.Tokens}") { + Tokens: LRegistry_by_pk(id: "${registryMap.Tokens}") { id numberOfAbsent numberOfRegistered @@ -53,11 +56,11 @@ const FETCH_ITEM_COUNTS_QUERY = gql` numberOfRegistrationRequested numberOfChallengedRegistrations registrationMetaEvidence { - URI + URI: uri } } } -`; +` const convertStringFieldsToNumber = (obj: any): any => { let result: any = Array.isArray(obj) ? [] : {} @@ -73,26 +76,41 @@ const convertStringFieldsToNumber = (obj: any): any => { } return result -}; +} export const useItemCountsQuery = (enabled: boolean = true) => { - const graphqlBatcher = useGraphqlBatcher(); - + const graphqlBatcher = useGraphqlBatcher() + return useQuery({ queryKey: queryKeys.itemCounts(), queryFn: async (): Promise => { - const requestId = crypto.randomUUID(); - const result = await graphqlBatcher.request(requestId, FETCH_ITEM_COUNTS_QUERY); - - const itemCounts: ItemCounts = convertStringFieldsToNumber(result); + const requestId = crypto.randomUUID() + const result = await graphqlBatcher.request( + requestId, + FETCH_ITEM_COUNTS_QUERY, + ) + + const itemCounts: ItemCounts = convertStringFieldsToNumber(result) // Fetch metadata for all registries const regMEs = await Promise.all([ - fetch('https://cdn.kleros.link' + result?.Single_Tags?.registrationMetaEvidence?.URI).then((r) => r.json()), - fetch('https://cdn.kleros.link' + result?.Tags_Queries?.registrationMetaEvidence?.URI).then((r) => r.json()), - fetch('https://cdn.kleros.link' + result?.CDN?.registrationMetaEvidence?.URI).then((r) => r.json()), - fetch('https://cdn.kleros.link' + result?.Tokens?.registrationMetaEvidence?.URI).then((r) => r.json()), - ]); + fetch( + 'https://cdn.kleros.link' + + result?.Single_Tags?.registrationMetaEvidence?.URI, + ).then((r) => r.json()), + fetch( + 'https://cdn.kleros.link' + + result?.Tags_Queries?.registrationMetaEvidence?.URI, + ).then((r) => r.json()), + fetch( + 'https://cdn.kleros.link' + + result?.CDN?.registrationMetaEvidence?.URI, + ).then((r) => r.json()), + fetch( + 'https://cdn.kleros.link' + + result?.Tokens?.registrationMetaEvidence?.URI, + ).then((r) => r.json()), + ]) // Inject metadata itemCounts.Single_Tags.metadata = { @@ -101,31 +119,31 @@ export const useItemCountsQuery = (enabled: boolean = true) => { logoURI: regMEs[0].metadata.logoURI, tcrTitle: regMEs[0].metadata.tcrTitle, tcrDescription: regMEs[0].metadata.tcrDescription, - }; - + } + itemCounts.Tags_Queries.metadata = { address: result?.Tags_Queries?.id, policyURI: regMEs[1].fileURI, logoURI: regMEs[1].metadata.logoURI, tcrTitle: regMEs[1].metadata.tcrTitle, tcrDescription: regMEs[1].metadata.tcrDescription, - }; - + } + itemCounts.CDN.metadata = { address: result?.CDN?.id, policyURI: regMEs[2].fileURI, logoURI: regMEs[2].metadata.logoURI, tcrTitle: regMEs[2].metadata.tcrTitle, tcrDescription: regMEs[2].metadata.tcrDescription, - }; - + } + itemCounts.Tokens.metadata = { address: result?.Tokens?.id, policyURI: regMEs[3].fileURI, logoURI: regMEs[3].metadata.logoURI, tcrTitle: regMEs[3].metadata.tcrTitle, tcrDescription: regMEs[3].metadata.tcrDescription, - }; + } // Fetch registry deposits const regDs = await Promise.all([ @@ -133,17 +151,17 @@ export const useItemCountsQuery = (enabled: boolean = true) => { fetchRegistryDeposits(registryMap.Tags_Queries), fetchRegistryDeposits(registryMap.CDN), fetchRegistryDeposits(registryMap.Tokens), - ]); + ]) - itemCounts.Single_Tags.deposits = regDs[0] as DepositParams; - itemCounts.Tags_Queries.deposits = regDs[1] as DepositParams; - itemCounts.CDN.deposits = regDs[2] as DepositParams; - itemCounts.Tokens.deposits = regDs[3] as DepositParams; + itemCounts.Single_Tags.deposits = regDs[0] as DepositParams + itemCounts.Tags_Queries.deposits = regDs[1] as DepositParams + itemCounts.CDN.deposits = regDs[2] as DepositParams + itemCounts.Tokens.deposits = regDs[3] as DepositParams - return itemCounts; + return itemCounts }, enabled, refetchInterval: REFETCH_INTERVAL, staleTime: STALE_TIME, - }); -}; \ No newline at end of file + }) +} diff --git a/websites/app/src/hooks/queries/useItemDetailsQuery.ts b/websites/app/src/hooks/queries/useItemDetailsQuery.ts index 30622bd..da85503 100644 --- a/websites/app/src/hooks/queries/useItemDetailsQuery.ts +++ b/websites/app/src/hooks/queries/useItemDetailsQuery.ts @@ -1,30 +1,28 @@ -import { useQuery } from '@tanstack/react-query'; -import { gql } from 'graphql-request'; -import { queryKeys, REFETCH_INTERVAL, STALE_TIME } from './consts'; -import { useGraphqlBatcher } from './useGraphqlBatcher'; -import { GraphItemDetails } from '../../utils/itemDetails'; +import { useQuery } from '@tanstack/react-query' +import { gql } from 'graphql-request' +import { queryKeys, REFETCH_INTERVAL, STALE_TIME } from './consts' +import { useGraphqlBatcher } from './useGraphqlBatcher' +import { GraphItemDetails } from '../../utils/itemDetails' const FETCH_ITEM_DETAILS_QUERY = gql` query FetchItemDetails($id: String!) { - litem(id: $id) { - metadata { - key0 - key1 - key2 - key3 - props { - value - type - label - description - isIdentifier - } + litem: LItem_by_pk(id: $id) { + key0 + key1 + key2 + key3 + props { + value + type: itemType + label + description + isIdentifier } itemID registryAddress status disputed - requests(orderBy: submissionTime, orderDirection: desc) { + requests(order_by: { submissionTime: desc }) { requestType disputed disputeID @@ -41,21 +39,19 @@ const FETCH_ITEM_DETAILS_QUERY = gql` resolutionTime evidenceGroup { id - evidences(orderBy: number, orderDirection: desc) { + evidences(order_by: { number: desc }) { party - URI + URI: uri number timestamp txHash - metadata { - title - description - fileURI - fileTypeExtension - } + title + description + fileURI + fileTypeExtension } } - rounds(orderBy: creationTime, orderDirection: desc) { + rounds(order_by: { creationTime: desc }) { appealed appealPeriodStart appealPeriodEnd @@ -71,28 +67,35 @@ const FETCH_ITEM_DETAILS_QUERY = gql` } } } -`; +` interface UseItemDetailsQueryParams { - itemId: string; - enabled?: boolean; + itemId: string + enabled?: boolean } -export const useItemDetailsQuery = ({ itemId, enabled = true }: UseItemDetailsQueryParams) => { - const graphqlBatcher = useGraphqlBatcher(); - +export const useItemDetailsQuery = ({ + itemId, + enabled = true, +}: UseItemDetailsQueryParams) => { + const graphqlBatcher = useGraphqlBatcher() + return useQuery({ queryKey: queryKeys.itemDetails(itemId), queryFn: async (): Promise => { - const requestId = crypto.randomUUID(); - const result = await graphqlBatcher.request(requestId, FETCH_ITEM_DETAILS_QUERY, { - id: itemId, - }); - - return result.litem; + const requestId = crypto.randomUUID() + const result = await graphqlBatcher.request( + requestId, + FETCH_ITEM_DETAILS_QUERY, + { + id: itemId, + }, + ) + + return result.litem }, enabled: enabled && itemId != null && itemId !== '', refetchInterval: REFETCH_INTERVAL, staleTime: STALE_TIME, - }); -}; \ No newline at end of file + }) +} diff --git a/websites/app/src/hooks/queries/useItemsQuery.ts b/websites/app/src/hooks/queries/useItemsQuery.ts index 3705338..f4fe64e 100644 --- a/websites/app/src/hooks/queries/useItemsQuery.ts +++ b/websites/app/src/hooks/queries/useItemsQuery.ts @@ -1,104 +1,114 @@ -import { useQuery } from '@tanstack/react-query'; -import { gql } from 'graphql-request'; -import { queryKeys, REFETCH_INTERVAL, STALE_TIME } from './consts'; -import { useGraphqlBatcher } from './useGraphqlBatcher'; -import { GraphItem, registryMap } from 'utils/items'; -import { ITEMS_PER_PAGE } from '../../pages/Registries/index'; -import { chains, getNamespaceForChainId } from '../../utils/chains'; +import { useQuery } from '@tanstack/react-query' +import { gql } from 'graphql-request' +import { queryKeys, REFETCH_INTERVAL, STALE_TIME } from './consts' +import { useGraphqlBatcher } from './useGraphqlBatcher' +import { GraphItem, registryMap } from 'utils/items' +import { ITEMS_PER_PAGE } from '../../pages/Registries/index' +import { chains, getNamespaceForChainId } from '../../utils/chains' interface UseItemsQueryParams { - searchParams: URLSearchParams; - chainFilters?: string[]; - enabled?: boolean; + searchParams: URLSearchParams + chainFilters?: string[] + enabled?: boolean } -export const useItemsQuery = ({ searchParams, chainFilters = [], enabled = true }: UseItemsQueryParams) => { - const graphqlBatcher = useGraphqlBatcher(); - - const registry = searchParams.getAll('registry'); - const status = searchParams.getAll('status'); - const disputed = searchParams.getAll('disputed'); - const network = chainFilters; - const text = searchParams.get('text') || ''; - const orderDirection = searchParams.get('orderDirection') || 'desc'; - const page = Number(searchParams.get('page')) || 1; - - const shouldFetch = enabled && - registry.length > 0 && - status.length > 0 && - disputed.length > 0 && - page > 0; +export const useItemsQuery = ({ + searchParams, + chainFilters = [], + enabled = true, +}: UseItemsQueryParams) => { + const graphqlBatcher = useGraphqlBatcher() + + const registry = searchParams.getAll('registry') + const status = searchParams.getAll('status') + const disputed = searchParams.getAll('disputed') + const network = chainFilters + const text = searchParams.get('text') || '' + const orderDirection = searchParams.get('orderDirection') || 'desc' + const page = Number(searchParams.get('page')) || 1 + + const shouldFetch = + enabled && + registry.length > 0 && + status.length > 0 && + disputed.length > 0 && + page > 0 return useQuery({ queryKey: [...queryKeys.items(searchParams), chainFilters], queryFn: async () => { - if (!shouldFetch) return []; + if (!shouldFetch) return [] + + const isTagsQueriesRegistry = registry.includes('Tags_Queries') + const selectedChainIds = network.filter((id) => id !== 'unknown') + const includeUnknown = network.includes('unknown') + const definedChainIds = chains.map((c) => c.id) - const isTagsQueriesRegistry = registry.includes('Tags_Queries'); - const selectedChainIds = network.filter((id) => id !== 'unknown'); - const includeUnknown = network.includes('unknown'); - const definedChainIds = chains.map((c) => c.id); - // Build network filter based on registry type - let networkQueryObject = ''; + let networkQueryObject = '' if (isTagsQueriesRegistry) { const conditions = selectedChainIds.map( (chainId) => - `{or: [{metadata_: {key2: "${chainId}"}}, {metadata_: {key1: "${chainId}"}}]}` - ); + `{ _or: [{ key2: { _eq: "${chainId}"}}, { key1: { _eq: "${chainId}"}}]}`, + ) if (includeUnknown) { conditions.push( - `{and: [{metadata_: {key1_not_in: $definedChainIds}}, {metadata_: {key2_not_in: $definedChainIds}}]}` - ); + `{ _and: [{ key1: { _nin: $definedChainIds}}, { key2: { _nin: $definedChainIds}}]}`, + ) } - networkQueryObject = conditions.length > 0 ? `{or: [${conditions.join(',')}]}` : '{}'; + networkQueryObject = + conditions.length > 0 ? `{_or: [${conditions.join(',')}]}` : '{}' } else { const conditions = selectedChainIds.map((chainId) => { - const namespace = getNamespaceForChainId(chainId); + const namespace = getNamespaceForChainId(chainId) if (namespace === 'solana') { - return `{metadata_: {key0_starts_with_nocase: "solana:"}}`; + return `{key0: { _ilike: "solana:%"}}` } - return `{metadata_: {key0_starts_with_nocase: "${namespace}:${chainId}:"}}`; - }); - networkQueryObject = conditions.length > 0 ? `{or: [${conditions.join(',')}]}` : '{}'; + return `{key0: {_ilike: "${namespace}:${chainId}:%"}}` + }) + networkQueryObject = + conditions.length > 0 ? `{_or: [${conditions.join(',')}]}` : '{}' } const textFilterObject = text - ? `{or: [ - {metadata_: {key0_contains_nocase: $text}}, - {metadata_: {key1_contains_nocase: $text}}, - {metadata_: {key2_contains_nocase: $text}}, - {metadata_: {key3_contains_nocase: $text}}, - {metadata_: {key4_contains_nocase: $text}} - ]}` - : ''; + ? `{_or: [ + {key0: {_ilike: $text}}, + {key1: {_ilike: $text}}, + {key2: {_ilike: $text}}, + {key3: {_ilike: $text}}, + {key4: {_ilike: $text}} + ]}` + : '' // Build the complete query with filters const queryWithFilters = gql` query FetchItems( - $registry: [String!]! - $status: [String!]! + $registry: [String] + $status: [status!]! $disputed: [Boolean!]! - $text: String! + $text: String $skip: Int! $first: Int! - $orderDirection: OrderDirection! - ${includeUnknown && isTagsQueriesRegistry ? '$definedChainIds: [String!]!' : ''} + $orderDirection: order_by! + ${ + includeUnknown && isTagsQueriesRegistry + ? '$definedChainIds: [String!]!' + : '' + } ) { - litems( + litems: LItem( where: { - and: [ - {registry_in: $registry}, - {status_in: $status}, - {disputed_in: $disputed}, - ${networkQueryObject}, - ${text === '' ? '' : textFilterObject} - ] + _and: [ + {registry_id: {_in :$registry}}, + {status: {_in: $status}}, + {disputed: {_in: $disputed}}, + ${networkQueryObject}, + ${text === '' ? '' : textFilterObject} + ] } - skip: $skip - first: $first - orderBy: "latestRequestSubmissionTime" - orderDirection: $orderDirection + offset: $skip + limit: $first + order_by: {latestRequestSubmissionTime : $orderDirection } ) { id latestRequestSubmissionTime @@ -107,21 +117,19 @@ export const useItemsQuery = ({ searchParams, chainFilters = [], enabled = true status disputed data - metadata { - key0 - key1 - key2 - key3 - key4 - props { - value - type - label - description - isIdentifier - } + key0 + key1 + key2 + key3 + key4 + props { + value + type: itemType + label + description + isIdentifier } - requests(first: 1, orderBy: submissionTime, orderDirection: desc) { + requests(limit: 1, order_by: {submissionTime: desc}) { disputed disputeID submissionTime @@ -130,7 +138,7 @@ export const useItemsQuery = ({ searchParams, chainFilters = [], enabled = true challenger resolutionTime deposit - rounds(first: 1, orderBy: creationTime, orderDirection: desc) { + rounds(limit: 1, order_by: {creationTime : desc}) { appealPeriodStart appealPeriodEnd ruling @@ -142,60 +150,79 @@ export const useItemsQuery = ({ searchParams, chainFilters = [], enabled = true } } } - `; + ` const variables: any = { - registry: registry.map((r) => registryMap[r]), + registry: registry.map((r) => registryMap[r]).filter((i) => i !== null), status, disputed: disputed.map((e) => e === 'true'), - text, skip: (page - 1) * ITEMS_PER_PAGE, first: ITEMS_PER_PAGE + 1, orderDirection, - }; + } + + if (text) { + variables.text = `%${text}%` + } if (includeUnknown && isTagsQueriesRegistry) { - variables.definedChainIds = definedChainIds; + variables.definedChainIds = definedChainIds } - const requestId = crypto.randomUUID(); - const result = await graphqlBatcher.request(requestId, queryWithFilters, variables); - - let items: GraphItem[] = result.litems; + const requestId = crypto.randomUUID() + const result = await graphqlBatcher.request( + requestId, + queryWithFilters, + variables, + ) + + let items: GraphItem[] = result.litems // Client-side filtering for non-Tags_Queries registries if (!isTagsQueriesRegistry && network.length > 0) { - const knownPrefixes = [...new Set(chains.map((chain) => { - if (chain.namespace === 'solana') { - return 'solana:'; - } - return `${chain.namespace}:${chain.id}:`; - }))]; + const knownPrefixes = [ + ...new Set( + chains.map((chain) => { + if (chain.namespace === 'solana') { + return 'solana:' + } + return `${chain.namespace}:${chain.id}:` + }), + ), + ] const selectedPrefixes = selectedChainIds.map((chainId) => { - const namespace = getNamespaceForChainId(chainId); + const namespace = getNamespaceForChainId(chainId) if (namespace === 'solana') { - return 'solana:'; + return 'solana:' } - return `${namespace}:${chainId}:`; - }); + return `${namespace}:${chainId}:` + }) items = items.filter((item: GraphItem) => { - const key0 = item.metadata?.key0?.toLowerCase() || ''; - const matchesSelectedChain = selectedPrefixes.length > 0 - ? selectedPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())) - : false; - - const isUnknownChain = !knownPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())); - - return (selectedPrefixes.length > 0 && matchesSelectedChain) || (includeUnknown && isUnknownChain); - }); + const key0 = item?.key0?.toLowerCase() || '' + const matchesSelectedChain = + selectedPrefixes.length > 0 + ? selectedPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) + : false + + const isUnknownChain = !knownPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) + + return ( + (selectedPrefixes.length > 0 && matchesSelectedChain) || + (includeUnknown && isUnknownChain) + ) + }) } - return items; + return items }, enabled: shouldFetch, refetchInterval: REFETCH_INTERVAL, staleTime: STALE_TIME, - }); -}; \ No newline at end of file + }) +} diff --git a/websites/app/src/hooks/useDapplookerStats.ts b/websites/app/src/hooks/useDapplookerStats.ts index fdc40db..2081d6f 100644 --- a/websites/app/src/hooks/useDapplookerStats.ts +++ b/websites/app/src/hooks/useDapplookerStats.ts @@ -1,81 +1,79 @@ -import { useQuery } from '@tanstack/react-query'; -import { gql } from 'graphql-request'; -import { useGraphqlBatcher } from './queries/useGraphqlBatcher'; -import { registryMap } from 'utils/items'; -import { DAPPLOOKER_API_KEY } from 'consts'; +import { useQuery } from '@tanstack/react-query' +import { gql } from 'graphql-request' +import { useGraphqlBatcher } from './queries/useGraphqlBatcher' +import { registryMap } from 'utils/items' +import { DAPPLOOKER_API_KEY } from 'consts' -type ChainName = 'ethereum' | 'polygon' | 'arbitrum' | 'optimism' | 'base'; +type ChainName = 'ethereum' | 'polygon' | 'arbitrum' | 'optimism' | 'base' interface RegistryData { - id: string; - numberOfRegistered: string; - numberOfAbsent: string; - numberOfRegistrationRequested: string; - numberOfClearingRequested: string; - numberOfChallengedRegistrations: string; - numberOfChallengedClearing: string; + id: string + numberOfRegistered: string + numberOfAbsent: string + numberOfRegistrationRequested: string + numberOfClearingRequested: string + numberOfChallengedRegistrations: string + numberOfChallengedClearing: string } interface ItemData { - id: string; - status: string; - disputed: boolean; - latestRequestSubmissionTime: string; - registryAddress: string; - metadata?: { - key0?: string; - key1?: string; - key2?: string; - key3?: string; - key4?: string; - }; + id: string + status: string + disputed: boolean + latestRequestSubmissionTime: string + registryAddress: string + key0?: string + key1?: string + key2?: string + key3?: string + key4?: string requests: Array<{ - submissionTime: string; - requester: string; - challenger?: string; - disputed: boolean; - deposit: string; - }>; + submissionTime: string + requester: string + challenger?: string + disputed: boolean + deposit: string + }> } interface SubgraphResponse { - lregistries: RegistryData[]; - litems: ItemData[]; + lregistries: RegistryData[] + litems: ItemData[] } interface DapplookerStatsData { - totalAssetsVerified: number; - totalSubmissions: number; - totalCurators: number; + totalAssetsVerified: number + totalSubmissions: number + totalCurators: number tokens: { - assetsVerified: number; - assetsVerifiedChange: number; - }; + assetsVerified: number + assetsVerifiedChange: number + } cdn: { - assetsVerified: number; - assetsVerifiedChange: number; - }; + assetsVerified: number + assetsVerifiedChange: number + } singleTags: { - assetsVerified: number; - assetsVerifiedChange: number; - }; + assetsVerified: number + assetsVerifiedChange: number + } tagQueries: { - assetsVerified: number; - assetsVerifiedChange: number; - }; + assetsVerified: number + assetsVerifiedChange: number + } submissionsVsDisputes: { - submissions: number[]; - disputes: number[]; - dates: string[]; - }; + submissions: number[] + disputes: number[] + dates: string[] + } chainRanking: { - rank: number; - chain: string; - items: number; - }[]; + rank: number + chain: string + items: number + }[] } -const DASHBOARD_ID = 'f5dcef21-ad65-4671-a930-58d3ec67f6a2'; +const DASHBOARD_ID = 'f5dcef21-ad65-4671-a930-58d3ec67f6a2' const CHAIN_PREFIXES: Record = { 'eip155:1:': 'ethereum', @@ -83,13 +81,13 @@ const CHAIN_PREFIXES: Record = { 'eip155:42161:': 'arbitrum', 'eip155:10:': 'optimism', 'eip155:8453:': 'base', -}; +} const CACHE_CONFIG = { staleTime: 5 * 60 * 1000, // 5 minutes refetchInterval: false, retry: 2, -} as const; +} as const const EMPTY_STATS: DapplookerStatsData = { totalAssetsVerified: 0, @@ -100,21 +98,24 @@ const EMPTY_STATS: DapplookerStatsData = { singleTags: { assetsVerified: 0, assetsVerifiedChange: 0 }, tagQueries: { assetsVerified: 0, assetsVerifiedChange: 0 }, submissionsVsDisputes: { submissions: [], disputes: [], dates: [] }, - chainRanking: [] -}; + chainRanking: [], +} const getCurateStatsQuery = () => { - const thirtyDaysAgo = Math.floor((Date.now() - 30 * 24 * 60 * 60 * 1000) / 1000); - + const thirtyDaysAgo = Math.floor( + (Date.now() - 30 * 24 * 60 * 60 * 1000) / 1000, + ) + return gql` query CurateStats { - lregistries( - where: { id_in: [ + lregistries: LRegistry( + where: { id:{ + _in: [ "0x66260c69d03837016d88c9877e61e08ef74c59f2", "0xae6aaed5434244be3699c56e7ebc828194f26dc3", "0x957a53a994860be4750810131d9c876b2f52d6e1", "0xee1502e29795ef6c2d60f8d7120596abe3bad990" - ]} + ]}} ) { id numberOfRegistered @@ -124,325 +125,380 @@ const getCurateStatsQuery = () => { numberOfChallengedRegistrations numberOfChallengedClearing } - litems( - first: 1000, - orderBy: latestRequestSubmissionTime, - orderDirection: desc, - where: { latestRequestSubmissionTime_gt: "${thirtyDaysAgo}" } + litems: LItem( + limit: 1000, + order_by: {latestRequestSubmissionTime : desc } + where: { latestRequestSubmissionTime: {_gt: "${thirtyDaysAgo}"} } ) { id status disputed latestRequestSubmissionTime registryAddress - requests(first: 1, orderBy: submissionTime, orderDirection: desc) { + requests(limit: 1, order_by: {submissionTime: desc}) { submissionTime requester challenger disputed deposit } - metadata { - key0 - key1 - key2 - key3 - key4 - } + key0 + key1 + key2 + key3 + key4 } } - `; -}; + ` +} // Query to get curator statistics const CURATOR_STATS_QUERY = gql` query CuratorStats { - litems(first: 1000) { + litems: LItem(limit: 1000) { requests { requester challenger } } } -`; - +` const getCardType = (cardName: string): string => { - if (cardName.includes('curators')) return 'curators'; - if (cardName.includes('total submissions')) return 'totalSubmissions'; - if (cardName.includes('chain ranking')) return 'chainRanking'; - if (cardName.includes('tokens v2')) return 'tokens'; - if (cardName.includes('cdn v2')) return 'cdn'; - if (cardName.includes('address tags v2')) return 'singleTags'; - if (cardName.includes('3x security registries')) return 'tagQueries'; - return 'unknown'; -}; - -const fetchDapplookerData = async (graphqlBatcher: any): Promise => { + if (cardName.includes('curators')) return 'curators' + if (cardName.includes('total submissions')) return 'totalSubmissions' + if (cardName.includes('chain ranking')) return 'chainRanking' + if (cardName.includes('tokens v2')) return 'tokens' + if (cardName.includes('cdn v2')) return 'cdn' + if (cardName.includes('address tags v2')) return 'singleTags' + if (cardName.includes('3x security registries')) return 'tagQueries' + return 'unknown' +} + +const fetchDapplookerData = async ( + graphqlBatcher: any, +): Promise => { if (!DAPPLOOKER_API_KEY) { - return null; + return null } try { - const apiUrl = `/api/dapplooker/public/api/public/dashboard/${DASHBOARD_ID}`; - + const apiUrl = `/api/dapplooker/public/api/public/dashboard/${DASHBOARD_ID}` + let dashboardResponse = await fetch(apiUrl, { method: 'GET', headers: { - 'Accept': 'application/json', + Accept: 'application/json', 'Content-Type': 'application/json', }, - }); - + }) + if (!dashboardResponse.ok) { - const fallbackUrl = `/api/dapplooker/public/api/public/dashboard/${DASHBOARD_ID}`; - + const fallbackUrl = `/api/dapplooker/public/api/public/dashboard/${DASHBOARD_ID}` + dashboardResponse = await fetch(fallbackUrl, { method: 'GET', headers: { - 'Accept': 'application/json', + Accept: 'application/json', 'Content-Type': 'application/json', }, - }); + }) } if (dashboardResponse.ok) { - const responseText = await dashboardResponse.text(); - - let dashboardInfo; + const responseText = await dashboardResponse.text() + + let dashboardInfo try { - dashboardInfo = JSON.parse(responseText); + dashboardInfo = JSON.parse(responseText) } catch (jsonError) { - return null; + return null } - - if (dashboardInfo.ordered_cards && Array.isArray(dashboardInfo.ordered_cards)) { - const cardDataPromises = []; - + + if ( + dashboardInfo.ordered_cards && + Array.isArray(dashboardInfo.ordered_cards) + ) { + const cardDataPromises = [] + for (const cardWrapper of dashboardInfo.ordered_cards) { - const card = cardWrapper.card; - if (!card?.id || !card?.name) continue; - - const cardName = card.name.toLowerCase(); - const isRelevantCard = + const card = cardWrapper.card + if (!card?.id || !card?.name) continue + + const cardName = card.name.toLowerCase() + const isRelevantCard = card.display === 'scalar' || cardName.includes('chain ranking') || cardName.includes('tokens v2 - submission') || cardName.includes('cdn v2 - submission') || cardName.includes('address tags v2 - submission') || - cardName.includes('3x security registries - stacked by registry'); - + cardName.includes('3x security registries - stacked by registry') + if (isRelevantCard) { - cardDataPromises.push(Promise.resolve({ - id: card.id, - name: card.name, - display: card.display, - cardType: getCardType(cardName) - })); + cardDataPromises.push( + Promise.resolve({ + id: card.id, + name: card.name, + display: card.display, + cardType: getCardType(cardName), + }), + ) } } - + if (cardDataPromises.length > 0) { - const cardResults = await Promise.all(cardDataPromises); - const validCardResults = cardResults.filter(Boolean); - + const cardResults = await Promise.all(cardDataPromises) + const validCardResults = cardResults.filter(Boolean) + if (validCardResults.length > 0) { - return await fetchKlerosSubgraphData(graphqlBatcher); + return await fetchKlerosSubgraphData(graphqlBatcher) } } } } - - return null; - + + return null } catch (error) { - return null; + return null } -}; - +} const getChainFromKey = (key0: string): ChainName | null => { for (const [prefix, chain] of Object.entries(CHAIN_PREFIXES)) { if (key0.startsWith(prefix)) { - return chain; + return chain } } - return null; -}; + return null +} -const calculateRegistryStats = (registries: RegistryData[], registryId: string, fallbackItems?: ItemData[]) => { +const calculateRegistryStats = ( + registries: RegistryData[], + registryId: string, + fallbackItems?: ItemData[], +) => { // Try direct match first - let registry = registries.find((r) => r.id.toLowerCase() === registryId.toLowerCase()); - + let registry = registries.find( + (r) => r.id.toLowerCase() === registryId.toLowerCase(), + ) + // If not found, try matching without '0x' prefix in case of format difference if (!registry && registryId.startsWith('0x')) { - registry = registries.find((r) => r.id.toLowerCase() === registryId.slice(2).toLowerCase()); + registry = registries.find( + (r) => r.id.toLowerCase() === registryId.slice(2).toLowerCase(), + ) } - + // If still not found, try adding '0x' prefix in case registry ID doesn't have it if (!registry && !registryId.startsWith('0x')) { - registry = registries.find((r) => r.id.toLowerCase() === `0x${registryId}`.toLowerCase()); + registry = registries.find( + (r) => r.id.toLowerCase() === `0x${registryId}`.toLowerCase(), + ) } - + if (!registry) { // Fallback: Calculate from items if available if (fallbackItems) { - const registryItems = fallbackItems.filter(item => - item.registryAddress?.toLowerCase() === registryId.toLowerCase() - ); - - const registered = registryItems.filter(item => item.status === 'Registered').length; - const registrationRequested = registryItems.filter(item => item.status === 'RegistrationRequested').length; - const clearingRequested = registryItems.filter(item => item.status === 'ClearingRequested').length; - const totalSubmissions = registered + registrationRequested + clearingRequested; - + const registryItems = fallbackItems.filter( + (item) => + item.registryAddress?.toLowerCase() === registryId.toLowerCase(), + ) + + const registered = registryItems.filter( + (item) => item.status === 'Registered', + ).length + const registrationRequested = registryItems.filter( + (item) => item.status === 'RegistrationRequested', + ).length + const clearingRequested = registryItems.filter( + (item) => item.status === 'ClearingRequested', + ).length + const totalSubmissions = + registered + registrationRequested + clearingRequested + return { assetsVerified: registered, - assetsVerifiedChange: totalSubmissions - }; + assetsVerifiedChange: totalSubmissions, + } } - - return { assetsVerified: 0, assetsVerifiedChange: 0 }; + + return { assetsVerified: 0, assetsVerifiedChange: 0 } } - - const registered = parseInt(registry.numberOfRegistered, 10) || 0; - const registrationRequested = parseInt(registry.numberOfRegistrationRequested, 10) || 0; - const clearingRequested = parseInt(registry.numberOfClearingRequested, 10) || 0; - - const totalSubmissions = registered + registrationRequested + clearingRequested; - + + const registered = parseInt(registry.numberOfRegistered, 10) || 0 + const registrationRequested = + parseInt(registry.numberOfRegistrationRequested, 10) || 0 + const clearingRequested = + parseInt(registry.numberOfClearingRequested, 10) || 0 + + const totalSubmissions = + registered + registrationRequested + clearingRequested + return { assetsVerified: registered, - assetsVerifiedChange: totalSubmissions - }; -}; + assetsVerifiedChange: totalSubmissions, + } +} const generateDateRange = (days: number): Date[] => { - const now = new Date(); + const now = new Date() return Array.from({ length: days }, (_, i) => { - const date = new Date(now); - date.setDate(date.getDate() - (days - 1 - i)); - return date; - }); -}; - -const filterItemsByDate = (items: ItemData[], targetDate: Date, includeDisputed = false): number => { - const dayStart = new Date(targetDate); - dayStart.setHours(0, 0, 0, 0); - const dayEnd = new Date(targetDate); - dayEnd.setHours(23, 59, 59, 999); - + const date = new Date(now) + date.setDate(date.getDate() - (days - 1 - i)) + return date + }) +} + +const filterItemsByDate = ( + items: ItemData[], + targetDate: Date, + includeDisputed = false, +): number => { + const dayStart = new Date(targetDate) + dayStart.setHours(0, 0, 0, 0) + const dayEnd = new Date(targetDate) + dayEnd.setHours(23, 59, 59, 999) + return items.filter((item) => { - const submissionTime = new Date(parseInt(item.latestRequestSubmissionTime, 10) * 1000); - const isInDateRange = submissionTime >= dayStart && submissionTime <= dayEnd; - return includeDisputed ? isInDateRange && item.disputed : isInDateRange; - }).length; -}; + const submissionTime = new Date( + parseInt(item.latestRequestSubmissionTime, 10) * 1000, + ) + const isInDateRange = submissionTime >= dayStart && submissionTime <= dayEnd + return includeDisputed ? isInDateRange && item.disputed : isInDateRange + }).length +} const calculateChainRanking = (items: ItemData[]) => { - const chainCounts = new Map(); - + const chainCounts = new Map() + items.forEach((item) => { - const key0 = item.metadata?.key0; - if (!key0) return; - - const chain = getChainFromKey(key0); + const key0 = item?.key0 + if (!key0) return + + const chain = getChainFromKey(key0) if (chain) { - chainCounts.set(chain, (chainCounts.get(chain) || 0) + 1); + chainCounts.set(chain, (chainCounts.get(chain) || 0) + 1) } - }); - + }) + return Array.from(chainCounts.entries()) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([chain, count], index) => ({ rank: index + 1, chain, - items: count - })); -}; - -const fetchKlerosSubgraphData = async (graphqlBatcher: any): Promise => { - const statsRequestId = `stats-${Date.now()}-${Math.random()}`; - const curatorRequestId = `curator-${Date.now()}-${Math.random()}`; - + items: count, + })) +} + +const fetchKlerosSubgraphData = async ( + graphqlBatcher: any, +): Promise => { + const statsRequestId = `stats-${Date.now()}-${Math.random()}` + const curatorRequestId = `curator-${Date.now()}-${Math.random()}` + const [statsResult, curatorResult] = await Promise.all([ - graphqlBatcher.request(statsRequestId, getCurateStatsQuery()) as Promise, - graphqlBatcher.request(curatorRequestId, CURATOR_STATS_QUERY) as Promise<{ litems: ItemData[] }> - ]); - - const registries = statsResult.lregistries || []; - const items = statsResult.litems || []; - const curatorItems = curatorResult.litems || []; - + graphqlBatcher.request( + statsRequestId, + getCurateStatsQuery(), + ) as Promise, + graphqlBatcher.request(curatorRequestId, CURATOR_STATS_QUERY) as Promise<{ + litems: ItemData[] + }>, + ]) + + const registries = statsResult.lregistries || [] + const items = statsResult.litems || [] + const curatorItems = curatorResult.litems || [] + // Calculate totals const totalVerified = registries.reduce((total, reg) => { - return total + (parseInt(reg.numberOfRegistered, 10) || 0); - }, 0); - + return total + (parseInt(reg.numberOfRegistered, 10) || 0) + }, 0) + const totalSubmissions = registries.reduce((total, reg) => { - return total + - (parseInt(reg.numberOfRegistered, 10) || 0) + - (parseInt(reg.numberOfRegistrationRequested, 10) || 0) + - (parseInt(reg.numberOfClearingRequested, 10) || 0); - }, 0); - + return ( + total + + (parseInt(reg.numberOfRegistered, 10) || 0) + + (parseInt(reg.numberOfRegistrationRequested, 10) || 0) + + (parseInt(reg.numberOfClearingRequested, 10) || 0) + ) + }, 0) + // Calculate unique curators - const uniqueCurators = new Set(); + const uniqueCurators = new Set() curatorItems.forEach((item) => { item.requests?.forEach((req) => { - if (req.requester) uniqueCurators.add(req.requester.toLowerCase()); - if (req.challenger) uniqueCurators.add(req.challenger.toLowerCase()); - }); - }); - + if (req.requester) uniqueCurators.add(req.requester.toLowerCase()) + if (req.challenger) uniqueCurators.add(req.challenger.toLowerCase()) + }) + }) + // Generate time series data - const last7Days = generateDateRange(30); - const submissionsData = last7Days.map(date => filterItemsByDate(items, date)); - const disputesData = last7Days.map(date => filterItemsByDate(items, date, true)); - const chainRanking = calculateChainRanking(items); + const last7Days = generateDateRange(30) + const submissionsData = last7Days.map((date) => + filterItemsByDate(items, date), + ) + const disputesData = last7Days.map((date) => + filterItemsByDate(items, date, true), + ) + const chainRanking = calculateChainRanking(items) - const tokensStats = calculateRegistryStats(registries, registryMap.Tokens, items); - const cdnStats = calculateRegistryStats(registries, registryMap.CDN, items); - const singleTagsStats = calculateRegistryStats(registries, registryMap.Single_Tags, items); - const tagQueriesStats = calculateRegistryStats(registries, registryMap.Tags_Queries, items); + const tokensStats = calculateRegistryStats( + registries, + registryMap.Tokens, + items, + ) + const cdnStats = calculateRegistryStats(registries, registryMap.CDN, items) + const singleTagsStats = calculateRegistryStats( + registries, + registryMap.Single_Tags, + items, + ) + const tagQueriesStats = calculateRegistryStats( + registries, + registryMap.Tags_Queries, + items, + ) return { totalAssetsVerified: totalVerified, totalSubmissions: totalSubmissions, totalCurators: uniqueCurators.size, tokens: tokensStats, - cdn: cdnStats, + cdn: cdnStats, singleTags: singleTagsStats, tagQueries: tagQueriesStats, submissionsVsDisputes: { submissions: submissionsData, disputes: disputesData, - dates: last7Days.map(d => d.toLocaleDateString('en', { month: 'short', day: 'numeric' })) + dates: last7Days.map((d) => + d.toLocaleDateString('en', { month: 'short', day: 'numeric' }), + ), }, - chainRanking - }; -}; + chainRanking, + } +} export const useDapplookerStats = () => { - const graphqlBatcher = useGraphqlBatcher(); - + const graphqlBatcher = useGraphqlBatcher() + return useQuery({ queryKey: ['dapplooker-stats'], queryFn: async (): Promise => { try { if (DAPPLOOKER_API_KEY) { - const enhancedData = await fetchDapplookerData(graphqlBatcher); + const enhancedData = await fetchDapplookerData(graphqlBatcher) if (enhancedData) { - return enhancedData; + return enhancedData } } - - return await fetchKlerosSubgraphData(graphqlBatcher); + + return await fetchKlerosSubgraphData(graphqlBatcher) } catch (error) { - return EMPTY_STATS; + return EMPTY_STATS } }, ...CACHE_CONFIG, - }); -}; \ No newline at end of file + }) +} diff --git a/websites/app/src/hooks/useSubmitterStats.ts b/websites/app/src/hooks/useSubmitterStats.ts index e5e3106..f5762fa 100644 --- a/websites/app/src/hooks/useSubmitterStats.ts +++ b/websites/app/src/hooks/useSubmitterStats.ts @@ -2,31 +2,54 @@ import { useQuery } from "@tanstack/react-query"; import { SUBGRAPH_GNOSIS_ENDPOINT } from "consts"; const SUBMITTER_STATS_QUERY = ` - query SubmitterStats($id: ID!) { - submitter(id: $id) { - totalSubmissions - ongoingSubmissions - pastSubmissions + query SubmitterStats($userAddress: String!) { + ongoing: LItem( + where: { + status: {_in: [RegistrationRequested, ClearingRequested]} + requests: {requester: {_eq: $userAddress}} + } + limit: 10000 + ) { + id + } + past: LItem( + where: { + status: {_in: [Registered, Absent]} + requests: {requester: {_eq: $userAddress}} + } + limit: 10000 + ) { + id } } `; export const useSubmitterStats = (address?: string) => { - const id = address?.toLowerCase(); + const userAddress = address?.toLowerCase(); return useQuery({ - queryKey: ["refetchOnBlock", "useSubmitterStats", id], - enabled: !!id, + queryKey: ["refetchOnBlock", "useSubmitterStats", userAddress], + enabled: !!userAddress, queryFn: async () => { const response = await fetch(SUBGRAPH_GNOSIS_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: SUBMITTER_STATS_QUERY, - variables: { id }, + variables: { userAddress }, }), }); const json = await response.json(); - return json.data; + + const ongoingCount = json.data?.ongoing?.length || 0; + const pastCount = json.data?.past?.length || 0; + + return { + submitter: { + totalSubmissions: ongoingCount + pastCount, + ongoingSubmissions: ongoingCount, + pastSubmissions: pastCount + } + }; }, }); }; diff --git a/websites/app/src/pages/Dashboard/Activity/ItemCard.tsx b/websites/app/src/pages/Dashboard/Activity/ItemCard.tsx index 8f1ec35..a4ba089 100644 --- a/websites/app/src/pages/Dashboard/Activity/ItemCard.tsx +++ b/websites/app/src/pages/Dashboard/Activity/ItemCard.tsx @@ -1,17 +1,17 @@ -import React, { useMemo } from "react"; -import styled from "styled-components"; -import { format } from "date-fns"; -import { formatEther } from "ethers"; -import { useNavigate } from "react-router-dom"; -import AddressDisplay from "components/AddressDisplay"; -import { revRegistryMap } from 'utils/items'; -import { useScrollTop } from "hooks/useScrollTop"; +import React, { useMemo } from 'react' +import styled from 'styled-components' +import { format } from 'date-fns' +import { formatEther } from 'ethers' +import { useNavigate } from 'react-router-dom' +import AddressDisplay from 'components/AddressDisplay' +import { revRegistryMap } from 'utils/items' +import { useScrollTop } from 'hooks/useScrollTop' import useHumanizedCountdown, { useChallengeRemainingTime, useChallengePeriodDuration, -} from "hooks/countdown"; -import { shortenAddress } from "~src/utils/shortenAddress"; -import HourglassIcon from "svgs/icons/hourglass.svg"; +} from 'hooks/countdown' +import { shortenAddress } from '~src/utils/shortenAddress' +import HourglassIcon from 'svgs/icons/hourglass.svg' const Card = styled.div` width: 100%; @@ -26,7 +26,7 @@ const Card = styled.div` rgba(255, 255, 255, 0.08) 0%, rgba(153, 153, 153, 0.08) 100% ); -`; +` const Header = styled.div` display: flex; @@ -34,7 +34,7 @@ const Header = styled.div` gap: 12px; padding: 14px 16px; flex-wrap: wrap; -`; +` const HeaderLeft = styled.div` display: inline-flex; @@ -42,7 +42,7 @@ const HeaderLeft = styled.div` flex-wrap: wrap; gap: 8px; min-width: 0; -`; +` const HeaderRight = styled.div` display: inline-flex; @@ -51,7 +51,7 @@ const HeaderRight = styled.div` color: ${({ theme }) => theme.secondaryText}; font-size: 14px; white-space: nowrap; -`; +` const Bullet = styled.span<{ color: string }>` width: 8px; @@ -59,36 +59,36 @@ const Bullet = styled.span<{ color: string }>` border-radius: 50%; background: ${({ color }) => color}; flex: 0 0 8px; -`; +` const Title = styled.span` font-size: 16px; font-weight: 600; color: ${({ theme }) => theme.primaryText}; -`; +` const Registry = styled.span` font-size: 14px; color: ${({ theme }) => theme.secondaryText}; -`; +` const StatusLabel = styled.span` font-size: 14px; color: ${({ theme }) => theme.primaryText}; -`; +` const Divider = styled.hr` border: none; border-top: 1px solid ${({ theme }) => theme.backgroundTwo}; margin: 0; -`; +` const Body = styled.div` padding: 16px; display: flex; flex-direction: column; gap: 12px; -`; +` const MetaLine = styled.div` display: flex; @@ -96,7 +96,7 @@ const MetaLine = styled.div` justify-content: space-between; flex-wrap: wrap; gap: 16px; -`; +` const InfoRow = styled.div` display: inline-flex; @@ -106,14 +106,14 @@ const InfoRow = styled.div` color: ${({ theme }) => theme.secondaryText}; flex: 1; min-width: 0; -`; +` const LabelValue = styled.span` display: flex; align-items: center; gap: 8px; white-space: nowrap; -`; +` const ViewButton = styled.button` position: relative; @@ -130,113 +130,117 @@ const ViewButton = styled.button` inset: 0; border-radius: 24px; padding: 1px; - background: linear-gradient(90deg, #8B5CF6 0%, #1C3CF1 100%); - -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + background: linear-gradient(90deg, #8b5cf6 0%, #1c3cf1 100%); + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); -webkit-mask-composite: xor; - mask-composite: exclude; + mask-composite: exclude; pointer-events: none; } -`; +` const StyledChainLabel = styled.span` margin-bottom: 8px; -`; +` const StyledChainContainer = styled(LabelValue)` margin-bottom: -8px; -`; +` const statusColors: Record = { - Included: "#65DC7F", - Removed: "#FF5A78", - "Registration Requested": "#60A5FA", - "Removal Requested": "#FBBF24", - Challenged: "#E87B35", -}; + Included: '#65DC7F', + Removed: '#FF5A78', + 'Registration Requested': '#60A5FA', + 'Removal Requested': '#FBBF24', + Challenged: '#E87B35', +} const readableStatus: Record = { - Registered: "Included", - Absent: "Removed", - RegistrationRequested: "Registration Requested", - ClearingRequested: "Removal Requested", -}; + Registered: 'Included', + Absent: 'Removed', + RegistrationRequested: 'Registration Requested', + ClearingRequested: 'Removal Requested', +} const challengedStatus: Record = { - RegistrationRequested: "Challenged", - ClearingRequested: "Challenged", -}; + RegistrationRequested: 'Challenged', + ClearingRequested: 'Challenged', +} const getProp = (item: any, label: string) => - item?.metadata?.props?.find((p: any) => p.label === label)?.value ?? ""; + item?.props?.find((p: any) => p.label === label)?.value ?? '' const ItemCard = ({ item }: { item: any }) => { - const navigate = useNavigate(); - const scrollTop = useScrollTop(); + const navigate = useNavigate() + const scrollTop = useScrollTop() - const registryName = revRegistryMap[item.registryAddress] ?? "Unknown"; + const registryName = revRegistryMap[item.registryAddress] ?? 'Unknown' const displayName = - getProp(item, "Name") || - getProp(item, "Domain name") || - getProp(item, "Public Name Tag") || - getProp(item, "Description") || - item.itemID; + getProp(item, 'Name') || + getProp(item, 'Domain name') || + getProp(item, 'Public Name Tag') || + getProp(item, 'Description') || + item.itemID const statusText = item.disputed - ? challengedStatus[item.status] || "Challenged" - : readableStatus[item.status] || item.status; + ? challengedStatus[item.status] || 'Challenged' + : readableStatus[item.status] || item.status - const bulletColor = statusColors[statusText] ?? "#9CA3AF"; + const bulletColor = statusColors[statusText] ?? '#9CA3AF' const submittedOn = item.requests?.[0]?.submissionTime != null - ? format(new Date(Number(item.requests[0].submissionTime) * 1000), "PP") - : "-"; + ? format(new Date(Number(item.requests[0].submissionTime) * 1000), 'PP') + : '-' const deposit = item.requests?.[0]?.deposit != null - ? Number(formatEther(item.requests[0].deposit)).toLocaleString("en-US", { + ? Number(formatEther(item.requests[0].deposit)).toLocaleString('en-US', { maximumFractionDigits: 0, }) - : "-"; + : '-' - const requester = item.requests?.[0]?.requester ?? ""; + const requester = item.requests?.[0]?.requester ?? '' - const chainId = getProp(item, "EVM Chain ID"); + const chainId = getProp(item, 'EVM Chain ID') const entryAddrMap: Record = { - Single_Tags: getProp(item, "Contract Address"), + Single_Tags: getProp(item, 'Contract Address'), Tags_Queries: undefined, - Tokens: getProp(item, "Address"), - CDN: getProp(item, "Contract address"), - }; - const entryAddr = entryAddrMap[registryName]; + Tokens: getProp(item, 'Address'), + CDN: getProp(item, 'Contract address'), + } + const entryAddr = entryAddrMap[registryName] - const challengePeriodDuration = useChallengePeriodDuration(item.registryAddress); + const challengePeriodDuration = useChallengePeriodDuration( + item.registryAddress, + ) const endsAtSeconds = useChallengeRemainingTime( item.requests?.[0]?.submissionTime, item.disputed, - challengePeriodDuration - ); - const endsIn = useHumanizedCountdown(endsAtSeconds, 2); + challengePeriodDuration, + ) + const endsIn = useHumanizedCountdown(endsAtSeconds, 2) const showEndsIn = useMemo( - () => Boolean(endsIn) && item.status !== "Registered", - [endsIn, item.status] - ); + () => Boolean(endsIn) && item.status !== 'Registered', + [endsIn, item.status], + ) const onView = () => { - const params = new URLSearchParams(); - params.set("registry", registryName); - params.set("status", "Registered"); - params.set("status", "RegistrationRequested"); - params.set("status", "ClearingRequested"); - params.set("status", "Absent"); - params.set("disputed", "true"); - params.set("disputed", "false"); - params.set("page", "1"); - params.set("orderDirection", "desc"); - navigate(`/item/${item.id}?${params.toString()}`); - scrollTop(); - }; + const params = new URLSearchParams() + params.set('registry', registryName) + params.set('status', 'Registered') + params.set('status', 'RegistrationRequested') + params.set('status', 'ClearingRequested') + params.set('status', 'Absent') + params.set('disputed', 'true') + params.set('disputed', 'false') + params.set('page', '1') + params.set('orderDirection', 'desc') + navigate(`/item/${item.id}?${params.toString()}`) + scrollTop() + } return ( @@ -295,7 +299,7 @@ const ItemCard = ({ item }: { item: any }) => { - ); -}; + ) +} -export default ItemCard; +export default ItemCard diff --git a/websites/app/src/pages/Dashboard/Activity/OngoingSubmissions.tsx b/websites/app/src/pages/Dashboard/Activity/OngoingSubmissions.tsx index 33acee3..8d2b80d 100644 --- a/websites/app/src/pages/Dashboard/Activity/OngoingSubmissions.tsx +++ b/websites/app/src/pages/Dashboard/Activity/OngoingSubmissions.tsx @@ -1,215 +1,242 @@ -import React, { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import Skeleton from "react-loading-skeleton"; -import ItemCard from "./ItemCard"; -import { useSearchParams, useNavigate, useLocation } from "react-router-dom"; -import { useScrollTop } from "hooks/useScrollTop"; -import { StyledPagination } from "components/StyledPagination"; -import { chains, getNamespaceForChainId } from "utils/chains"; -import { SUBGRAPH_GNOSIS_ENDPOINT } from "consts"; +import React, { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import Skeleton from 'react-loading-skeleton' +import ItemCard from './ItemCard' +import { useSearchParams, useNavigate, useLocation } from 'react-router-dom' +import { useScrollTop } from 'hooks/useScrollTop' +import { StyledPagination } from 'components/StyledPagination' +import { chains, getNamespaceForChainId } from 'utils/chains' +import { SUBGRAPH_GNOSIS_ENDPOINT } from 'consts' const QUERY = ` - query OngoingItems( - $userAddress: Bytes!, - $first: Int!, - $skip: Int!, - $status: [Status!]!, - $disputed: [Boolean!]!, - $orderDirection: OrderDirection! +query OngoingItems($userAddress: String!, $first: Int!, $skip: Int!, $status: [status!]!, $disputed: [Boolean!]!, $orderDirection: order_by!) { + litems: LItem( + where: {status: {_in: $status}, disputed: {_in: $disputed}, requests: {requester: {_eq: $userAddress}}} + limit: $first + offset: $skip + order_by: {latestRequestSubmissionTime: $orderDirection} ) { - litems( - where: { - status_in: $status - disputed_in: $disputed - requests_: { requester: $userAddress } - } - first: $first - skip: $skip - orderBy: latestRequestSubmissionTime - orderDirection: $orderDirection - ) { - id - itemID - registryAddress - latestRequestSubmissionTime - status + id + itemID + registryAddress + latestRequestSubmissionTime + status + disputed + data + key0 + key1 + key2 + key3 + key4 + props { + value + label + } + requests(limit: 1, order_by: {submissionTime: desc}) { + requester + deposit + submissionTime disputed - data - metadata { - key0 - key1 - key2 - key3 - key4 - props { - value - label - } - } - requests(first: 1, orderBy: submissionTime, orderDirection: desc) { - requester - deposit - submissionTime - disputed - } } } -`; +} +` interface Props { - totalItems: number; - address?: string; - chainFilters?: string[]; + totalItems: number + address?: string + chainFilters?: string[] } -const OngoingSubmissions: React.FC = ({ totalItems, address, chainFilters = [] }) => { - const [searchParams] = useSearchParams(); - const currentPage = parseInt(searchParams.get("page") ?? "1", 10); - const itemsPerPage = 10; - const navigate = useNavigate(); - const location = useLocation(); - const scrollTop = useScrollTop(); - const queryAddress = address?.toLowerCase(); - +const OngoingSubmissions: React.FC = ({ + totalItems, + address, + chainFilters = [], +}) => { + const [searchParams] = useSearchParams() + const currentPage = parseInt(searchParams.get('page') ?? '1', 10) + const itemsPerPage = 10 + const navigate = useNavigate() + const location = useLocation() + const scrollTop = useScrollTop() + const queryAddress = address?.toLowerCase() + // Get filter parameters with defaults for ongoing submissions - const statusParams = searchParams.getAll('status'); - const disputedParams = searchParams.getAll('disputed'); - const orderDirection = searchParams.get('orderDirection') || 'desc'; - + const statusParams = searchParams.getAll('status') + const disputedParams = searchParams.getAll('disputed') + const orderDirection = searchParams.get('orderDirection') || 'desc' + // Default ongoing statuses - exclude Registered and Absent - const defaultOngoingStatuses = ['RegistrationRequested', 'ClearingRequested']; - const status = statusParams.length > 0 ? statusParams.filter(s => defaultOngoingStatuses.includes(s)) : defaultOngoingStatuses; - const disputed = disputedParams.length > 0 ? disputedParams.map(d => d === 'true') : [true, false]; - - const searchTerm = searchParams.get('search') || ''; - + const defaultOngoingStatuses = ['RegistrationRequested', 'ClearingRequested'] + const status = + statusParams.length > 0 + ? statusParams.filter((s) => defaultOngoingStatuses.includes(s)) + : defaultOngoingStatuses + const disputed = + disputedParams.length > 0 + ? disputedParams.map((d) => d === 'true') + : [true, false] + + const searchTerm = searchParams.get('search') || '' + const { data, isLoading } = useQuery({ - queryKey: ["ongoingItems", queryAddress, currentPage, status, disputed, orderDirection, chainFilters, searchTerm], + queryKey: [ + 'ongoingItems', + queryAddress, + currentPage, + status, + disputed, + orderDirection, + chainFilters, + searchTerm, + ], enabled: !!queryAddress, queryFn: async () => { // Fetch more items to properly calculate filtered totals // We'll fetch up to 1000 items and handle pagination client-side - const fetchSize = Math.max(1000, currentPage * itemsPerPage); + const fetchSize = Math.max(1000, currentPage * itemsPerPage) const res = await fetch(SUBGRAPH_GNOSIS_ENDPOINT, { - method: "POST", - headers: { "Content-Type": "application/json" }, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: QUERY, - variables: { - userAddress: queryAddress, - first: fetchSize, + variables: { + userAddress: queryAddress, + first: fetchSize, skip: 0, // Always start from 0 to get accurate totals status, disputed, - orderDirection + orderDirection, }, }), - }); - const json = await res.json(); - if (json.errors) return { items: [], totalFiltered: 0 }; - let items = json.data.litems as any[]; + }) + const json = await res.json() + if (json.errors) return { items: [], totalFiltered: 0 } + let items = json.data.litems as any[] // Client-side filtering by chains if (chainFilters.length > 0) { - const selectedChainIds = chainFilters.filter((id) => id !== 'unknown'); - const includeUnknown = chainFilters.includes('unknown'); - - const knownPrefixes = [...new Set(chains.map((chain) => { - if (chain.namespace === 'solana') { - return 'solana:'; - } - return `${chain.namespace}:${chain.id}:`; - }))]; + const selectedChainIds = chainFilters.filter((id) => id !== 'unknown') + const includeUnknown = chainFilters.includes('unknown') + + const knownPrefixes = [ + ...new Set( + chains.map((chain) => { + if (chain.namespace === 'solana') { + return 'solana:' + } + return `${chain.namespace}:${chain.id}:` + }), + ), + ] const selectedPrefixes = selectedChainIds.map((chainId) => { - const namespace = getNamespaceForChainId(chainId); + const namespace = getNamespaceForChainId(chainId) if (namespace === 'solana') { - return 'solana:'; + return 'solana:' } - return `${namespace}:${chainId}:`; - }); + return `${namespace}:${chainId}:` + }) items = items.filter((item: any) => { - const key0 = item.metadata?.key0?.toLowerCase() || ''; - const matchesSelectedChain = selectedPrefixes.length > 0 - ? selectedPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())) - : false; + const key0 = item?.key0?.toLowerCase() || '' + const matchesSelectedChain = + selectedPrefixes.length > 0 + ? selectedPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) + : false - const isUnknownChain = !knownPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())); + const isUnknownChain = !knownPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) - return (selectedPrefixes.length > 0 && matchesSelectedChain) || (includeUnknown && isUnknownChain); - }); + return ( + (selectedPrefixes.length > 0 && matchesSelectedChain) || + (includeUnknown && isUnknownChain) + ) + }) } // Client-side text search filtering if (searchTerm) { - const searchLower = searchTerm.toLowerCase(); + const searchLower = searchTerm.toLowerCase() items = items.filter((item: any) => { // Search through metadata props - const propsText = item.metadata?.props?.map((prop: any) => `${prop.label}: ${prop.value}`).join(' ').toLowerCase() || ''; - + const propsText = + item?.props + ?.map((prop: any) => `${prop.label}: ${prop.value}`) + .join(' ') + .toLowerCase() || '' + // Search through keys const keysText = [ - item.metadata?.key0, - item.metadata?.key1, - item.metadata?.key2, - item.metadata?.key3, - item.metadata?.key4 - ].filter(Boolean).join(' ').toLowerCase(); - + item?.key0, + item?.key1, + item?.key2, + item?.key3, + item?.key4, + ] + .filter(Boolean) + .join(' ') + .toLowerCase() + // Search through basic fields - const basicText = [ - item.id, - item.itemID, - item.registryAddress - ].filter(Boolean).join(' ').toLowerCase(); - - const searchableText = `${propsText} ${keysText} ${basicText}`; - return searchableText.includes(searchLower); - }); + const basicText = [item.id, item.itemID, item.registryAddress] + .filter(Boolean) + .join(' ') + .toLowerCase() + + const searchableText = `${propsText} ${keysText} ${basicText}` + return searchableText.includes(searchLower) + }) } // Calculate total filtered count - const totalFiltered = items.length; - + const totalFiltered = items.length + // Apply pagination to filtered results - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedItems = items.slice(startIndex, endIndex); + const startIndex = (currentPage - 1) * itemsPerPage + const endIndex = startIndex + itemsPerPage + const paginatedItems = items.slice(startIndex, endIndex) - return { items: paginatedItems, totalFiltered }; + return { items: paginatedItems, totalFiltered } }, - }); + }) const totalPages = useMemo(() => { - if (!data) return 1; - return Math.max(1, Math.ceil(data.totalFiltered / itemsPerPage)); - }, [data]); - + if (!data) return 1 + return Math.max(1, Math.ceil(data.totalFiltered / itemsPerPage)) + }, [data]) + const handlePageChange = (newPage: number) => { - scrollTop(true); - const params = new URLSearchParams(location.search); - params.set("page", String(newPage)); - navigate(`${location.pathname}?${params.toString()}`); - }; - + scrollTop(true) + const params = new URLSearchParams(location.search) + params.set('page', String(newPage)) + navigate(`${location.pathname}?${params.toString()}`) + } + if (isLoading) return ( <> - ); - if (!data || data.items.length === 0) return <>No ongoing submissions.; - + ) + if (!data || data.items.length === 0) return <>No ongoing submissions. + return ( <> {data.items.map((item) => ( ))} {totalPages > 1 && ( - + )} - ); -}; + ) +} -export default OngoingSubmissions; +export default OngoingSubmissions diff --git a/websites/app/src/pages/Dashboard/Activity/PastSubmissions.tsx b/websites/app/src/pages/Dashboard/Activity/PastSubmissions.tsx index 5947745..91a74af 100644 --- a/websites/app/src/pages/Dashboard/Activity/PastSubmissions.tsx +++ b/websites/app/src/pages/Dashboard/Activity/PastSubmissions.tsx @@ -1,212 +1,239 @@ -import React, { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import Skeleton from "react-loading-skeleton"; -import ItemCard from "./ItemCard"; -import { useSearchParams, useNavigate, useLocation } from "react-router-dom"; -import { useScrollTop } from "hooks/useScrollTop"; -import { StyledPagination } from "components/StyledPagination"; -import { chains, getNamespaceForChainId } from "utils/chains"; -import { SUBGRAPH_GNOSIS_ENDPOINT } from "consts"; +import React, { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import Skeleton from 'react-loading-skeleton' +import ItemCard from './ItemCard' +import { useSearchParams, useNavigate, useLocation } from 'react-router-dom' +import { useScrollTop } from 'hooks/useScrollTop' +import { StyledPagination } from 'components/StyledPagination' +import { chains, getNamespaceForChainId } from 'utils/chains' +import { SUBGRAPH_GNOSIS_ENDPOINT } from 'consts' const QUERY = ` - query PastItems( - $userAddress: Bytes!, - $first: Int!, - $skip: Int!, - $status: [Status!]!, - $disputed: [Boolean!]!, - $orderDirection: OrderDirection! +query PastItems($userAddress: String!, $first: Int!, $skip: Int!, $status: [status!]!, $disputed: [Boolean!]!, $orderDirection: order_by!) { + litems: LItem( + where: {status: {_in: $status}, disputed: {_in: $disputed}, requests: {requester: {_eq: $userAddress}}} + limit: $first + offset: $skip + order_by: {latestRequestSubmissionTime: $orderDirection} ) { - litems( - where: { - status_in: $status - disputed_in: $disputed - requests_: { requester: $userAddress } - } - first: $first - skip: $skip - orderBy: latestRequestSubmissionTime - orderDirection: $orderDirection - ) { - id - itemID - registryAddress - latestRequestSubmissionTime - status - disputed - data - metadata { - key0 - key1 - key2 - key3 - key4 - props { - value - label - } - } - requests(first: 1, orderBy: submissionTime, orderDirection: desc) { - requester - deposit - submissionTime - resolutionTime - } + id + itemID + registryAddress + latestRequestSubmissionTime + status + disputed + data + key0 + key1 + key2 + key3 + key4 + props { + value + label + } + requests(limit: 1, order_by: {submissionTime: desc}) { + requester + deposit + submissionTime + resolutionTime } } -`; +} +` interface Props { - totalItems: number; - address?: string; - chainFilters?: string[]; + totalItems: number + address?: string + chainFilters?: string[] } -const PastSubmissions: React.FC = ({ totalItems, address, chainFilters = [] }) => { - const [searchParams] = useSearchParams(); - const currentPage = parseInt(searchParams.get("page") ?? "1", 10); - const itemsPerPage = 20; - const skip = itemsPerPage * (currentPage - 1); - const navigate = useNavigate(); - const location = useLocation(); - const scrollTop = useScrollTop(); - const queryAddress = address?.toLowerCase(); - - const statusParams = searchParams.getAll('status'); - const disputedParams = searchParams.getAll('disputed'); - const orderDirection = searchParams.get('orderDirection') || 'desc'; - - const defaultPastStatuses = ['Registered', 'Absent']; - const status = statusParams.length > 0 ? statusParams.filter(s => defaultPastStatuses.includes(s)) : defaultPastStatuses; - const disputed = disputedParams.length > 0 ? disputedParams.map(d => d === 'true') : [true, false]; - - const searchTerm = searchParams.get('search') || ''; - +const PastSubmissions: React.FC = ({ + totalItems, + address, + chainFilters = [], +}) => { + const [searchParams] = useSearchParams() + const currentPage = parseInt(searchParams.get('page') ?? '1', 10) + const itemsPerPage = 20 + const skip = itemsPerPage * (currentPage - 1) + const navigate = useNavigate() + const location = useLocation() + const scrollTop = useScrollTop() + const queryAddress = address?.toLowerCase() + + const statusParams = searchParams.getAll('status') + const disputedParams = searchParams.getAll('disputed') + const orderDirection = searchParams.get('orderDirection') || 'desc' + + const defaultPastStatuses = ['Registered', 'Absent'] + const status = + statusParams.length > 0 + ? statusParams.filter((s) => defaultPastStatuses.includes(s)) + : defaultPastStatuses + const disputed = + disputedParams.length > 0 + ? disputedParams.map((d) => d === 'true') + : [true, false] + + const searchTerm = searchParams.get('search') || '' + const { data, isLoading } = useQuery({ - queryKey: ["pastItems", queryAddress, currentPage, status, disputed, orderDirection, chainFilters, searchTerm], + queryKey: [ + 'pastItems', + queryAddress, + currentPage, + status, + disputed, + orderDirection, + chainFilters, + searchTerm, + ], enabled: !!queryAddress, queryFn: async () => { - const fetchSize = Math.max(1000, currentPage * itemsPerPage); + const fetchSize = Math.max(1000, currentPage * itemsPerPage) const res = await fetch(SUBGRAPH_GNOSIS_ENDPOINT, { - method: "POST", - headers: { "Content-Type": "application/json" }, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: QUERY, - variables: { - userAddress: queryAddress, - first: fetchSize, + variables: { + userAddress: queryAddress, + first: fetchSize, skip: 0, status, disputed, - orderDirection + orderDirection, }, }), - }); - const json = await res.json(); - if (json.errors) return { items: [], totalFiltered: 0 }; - let items = json.data.litems as any[]; + }) + const json = await res.json() + if (json.errors) return { items: [], totalFiltered: 0 } + let items = json.data.litems as any[] // Client-side filtering by chains if (chainFilters.length > 0) { - const selectedChainIds = chainFilters.filter((id) => id !== 'unknown'); - const includeUnknown = chainFilters.includes('unknown'); - - const knownPrefixes = [...new Set(chains.map((chain) => { - if (chain.namespace === 'solana') { - return 'solana:'; - } - return `${chain.namespace}:${chain.id}:`; - }))]; + const selectedChainIds = chainFilters.filter((id) => id !== 'unknown') + const includeUnknown = chainFilters.includes('unknown') + + const knownPrefixes = [ + ...new Set( + chains.map((chain) => { + if (chain.namespace === 'solana') { + return 'solana:' + } + return `${chain.namespace}:${chain.id}:` + }), + ), + ] const selectedPrefixes = selectedChainIds.map((chainId) => { - const namespace = getNamespaceForChainId(chainId); + const namespace = getNamespaceForChainId(chainId) if (namespace === 'solana') { - return 'solana:'; + return 'solana:' } - return `${namespace}:${chainId}:`; - }); + return `${namespace}:${chainId}:` + }) items = items.filter((item: any) => { - const key0 = item.metadata?.key0?.toLowerCase() || ''; - const matchesSelectedChain = selectedPrefixes.length > 0 - ? selectedPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())) - : false; + const key0 = item?.key0?.toLowerCase() || '' + const matchesSelectedChain = + selectedPrefixes.length > 0 + ? selectedPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) + : false - const isUnknownChain = !knownPrefixes.some((prefix) => key0.startsWith(prefix.toLowerCase())); + const isUnknownChain = !knownPrefixes.some((prefix) => + key0.startsWith(prefix.toLowerCase()), + ) - return (selectedPrefixes.length > 0 && matchesSelectedChain) || (includeUnknown && isUnknownChain); - }); + return ( + (selectedPrefixes.length > 0 && matchesSelectedChain) || + (includeUnknown && isUnknownChain) + ) + }) } // Client-side text search filtering if (searchTerm) { - const searchLower = searchTerm.toLowerCase(); + const searchLower = searchTerm.toLowerCase() items = items.filter((item: any) => { // Search through metadata props - const propsText = item.metadata?.props?.map((prop: any) => `${prop.label}: ${prop.value}`).join(' ').toLowerCase() || ''; - + const propsText = + item?.props + ?.map((prop: any) => `${prop.label}: ${prop.value}`) + .join(' ') + .toLowerCase() || '' + // Search through keys const keysText = [ - item.metadata?.key0, - item.metadata?.key1, - item.metadata?.key2, - item.metadata?.key3, - item.metadata?.key4 - ].filter(Boolean).join(' ').toLowerCase(); - + item?.key0, + item?.key1, + item?.key2, + item?.key3, + item?.key4, + ] + .filter(Boolean) + .join(' ') + .toLowerCase() + // Search through basic fields - const basicText = [ - item.id, - item.itemID, - item.registryAddress - ].filter(Boolean).join(' ').toLowerCase(); - - const searchableText = `${propsText} ${keysText} ${basicText}`; - return searchableText.includes(searchLower); - }); + const basicText = [item.id, item.itemID, item.registryAddress] + .filter(Boolean) + .join(' ') + .toLowerCase() + + const searchableText = `${propsText} ${keysText} ${basicText}` + return searchableText.includes(searchLower) + }) } // Calculate total filtered count - const totalFiltered = items.length; - + const totalFiltered = items.length + // Apply pagination to filtered results - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedItems = items.slice(startIndex, endIndex); + const startIndex = (currentPage - 1) * itemsPerPage + const endIndex = startIndex + itemsPerPage + const paginatedItems = items.slice(startIndex, endIndex) - return { items: paginatedItems, totalFiltered }; + return { items: paginatedItems, totalFiltered } }, - }); + }) const totalPages = useMemo(() => { - if (!data) return 1; - return Math.max(1, Math.ceil(data.totalFiltered / itemsPerPage)); - }, [data]); - + if (!data) return 1 + return Math.max(1, Math.ceil(data.totalFiltered / itemsPerPage)) + }, [data]) + const handlePageChange = (newPage: number) => { - scrollTop(true); - const params = new URLSearchParams(location.search); - params.set("page", String(newPage)); - navigate(`${location.pathname}?${params.toString()}`); - }; - + scrollTop(true) + const params = new URLSearchParams(location.search) + params.set('page', String(newPage)) + navigate(`${location.pathname}?${params.toString()}`) + } + if (isLoading) return ( <> - ); - if (!data || data.items.length === 0) return <>No past submissions.; - + ) + if (!data || data.items.length === 0) return <>No past submissions. + return ( <> {data.items.map((item) => ( ))} {totalPages > 1 && ( - + )} - ); -}; + ) +} -export default PastSubmissions; +export default PastSubmissions diff --git a/websites/app/src/pages/ItemDetails/index.tsx b/websites/app/src/pages/ItemDetails/index.tsx index b27a264..081b87e 100644 --- a/websites/app/src/pages/ItemDetails/index.tsx +++ b/websites/app/src/pages/ItemDetails/index.tsx @@ -1,28 +1,30 @@ -import React, { useMemo, useState } from 'react'; -import styled, { css } from 'styled-components'; -import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; -import { formatEther } from 'ethers'; -import { useQuery } from '@tanstack/react-query'; -import { landscapeStyle } from 'styles/landscapeStyle'; -import { responsiveSize } from 'styles/responsiveSize'; -import ReactMarkdown from 'react-markdown'; -import { renderValue, StyledWebsiteAnchor } from 'utils/renderValue'; -import { fetchArbitrationCost } from 'utils/fetchArbitrationCost'; -import { revRegistryMap } from 'utils/items'; -import { useItemDetailsQuery, useItemCountsQuery } from 'hooks/queries'; -import { formatTimestamp } from 'utils/formatTimestamp'; -import { getStatusLabel } from 'utils/getStatusLabel'; -import LoadingItems from '../Registries/LoadingItems'; -import ConfirmationBox from 'components/ConfirmationBox'; -import { SubmitButton } from '../Registries/SubmitEntries/AddEntryModal'; -import AttachmentIcon from "assets/svgs/icons/attachment.svg"; -import useHumanizedCountdown, { useChallengeRemainingTime } from 'hooks/countdown'; -import { useChallengePeriodDuration } from 'hooks/countdown'; -import AddressDisplay from 'components/AddressDisplay'; -import SubmittedByLink from 'components/SubmittedByLink'; -import { useScrollTop } from 'hooks/useScrollTop'; -import ArrowLeftIcon from "assets/svgs/icons/arrow-left.svg"; -import EvidenceAttachmentDisplay from 'components/AttachmentDisplay'; +import React, { useMemo, useState } from 'react' +import styled, { css } from 'styled-components' +import { useParams, useNavigate, useSearchParams } from 'react-router-dom' +import { formatEther } from 'ethers' +import { useQuery } from '@tanstack/react-query' +import { landscapeStyle } from 'styles/landscapeStyle' +import { responsiveSize } from 'styles/responsiveSize' +import ReactMarkdown from 'react-markdown' +import { renderValue, StyledWebsiteAnchor } from 'utils/renderValue' +import { fetchArbitrationCost } from 'utils/fetchArbitrationCost' +import { revRegistryMap } from 'utils/items' +import { useItemDetailsQuery, useItemCountsQuery } from 'hooks/queries' +import { formatTimestamp } from 'utils/formatTimestamp' +import { getStatusLabel } from 'utils/getStatusLabel' +import LoadingItems from '../Registries/LoadingItems' +import ConfirmationBox from 'components/ConfirmationBox' +import { SubmitButton } from '../Registries/SubmitEntries/AddEntryModal' +import AttachmentIcon from 'assets/svgs/icons/attachment.svg' +import useHumanizedCountdown, { + useChallengeRemainingTime, +} from 'hooks/countdown' +import { useChallengePeriodDuration } from 'hooks/countdown' +import AddressDisplay from 'components/AddressDisplay' +import SubmittedByLink from 'components/SubmittedByLink' +import { useScrollTop } from 'hooks/useScrollTop' +import ArrowLeftIcon from 'assets/svgs/icons/arrow-left.svg' +import EvidenceAttachmentDisplay from 'components/AttachmentDisplay' const Container = styled.div` display: flex; @@ -30,15 +32,15 @@ const Container = styled.div` color: ${({ theme }) => theme.primaryText}; min-height: 100vh; padding: 32px 16px 64px; - font-family: "Inter", sans-serif; + font-family: 'Inter', sans-serif; background: ${({ theme }) => theme.lightBackground}; ${landscapeStyle( () => css` padding: 48px ${responsiveSize(0, 48)} 60px; - ` + `, )} -`; +` const Header = styled.div` display: flex; @@ -46,7 +48,7 @@ const Header = styled.div` align-items: center; gap: 16px; margin-bottom: 32px; -`; +` const BackButton = styled.button` background: none; @@ -69,20 +71,20 @@ const BackButton = styled.button` width: 20px; height: 20px; } -`; +` const Title = styled.h1` font-size: 24px; margin: 0; flex: 1; -`; +` const TabsWrapper = styled.div` display: flex; gap: 40px; border-bottom: 1px solid ${({ theme }) => theme.lightGrey}; margin-bottom: 24px; -`; +` const TabButton = styled.button<{ selected: boolean }>` background: none; @@ -90,13 +92,15 @@ const TabButton = styled.button<{ selected: boolean }>` padding: 0 0 12px; font-size: 18px; font-weight: 600; - color: ${({ theme, selected }) => (selected ? theme.primaryText : theme.secondaryText)}; - border-bottom: 3px solid ${({ theme, selected }) => (selected ? theme.primaryText : "transparent")}; + color: ${({ theme, selected }) => + selected ? theme.primaryText : theme.secondaryText}; + border-bottom: 3px solid + ${({ theme, selected }) => (selected ? theme.primaryText : 'transparent')}; cursor: pointer; &:hover { color: ${({ theme }) => theme.primaryText}; } -`; +` const ContentWrapper = styled.div` display: flex; @@ -106,9 +110,9 @@ const ContentWrapper = styled.div` border-radius: 12px; padding: ${responsiveSize(16, 24)}; word-break: break-word; -`; +` -const StatusButton = styled.button<{ status?: string; }>` +const StatusButton = styled.button<{ status?: string }>` background-color: #cd9dff; color: #380c65; padding: 12px 24px; @@ -132,11 +136,11 @@ const StatusButton = styled.button<{ status?: string; }>` ${landscapeStyle( () => css` padding: 12px 20px; - ` + `, )} -`; +` -const StatusSpan = styled.span<{ status: string; }>` +const StatusSpan = styled.span<{ status: string }>` display: inline-flex; align-items: center; gap: 8px; @@ -145,23 +149,24 @@ const StatusSpan = styled.span<{ status: string; }>` font-weight: 600; color: ${({ status }) => { const colors = { - 'Registered': '#48BB78', - 'RegistrationRequested': '#ED8936', - 'ClearingRequested': '#D69E2E', - 'Absent': '#718096', - }; - return colors[status] || '#718096'; + Registered: '#48BB78', + RegistrationRequested: '#ED8936', + ClearingRequested: '#D69E2E', + Absent: '#718096', + } + return colors[status] || '#718096' }}; border-radius: 20px; - border: 2px solid ${({ status }) => { - const colors = { - 'Registered': '#48BB78', - 'RegistrationRequested': '#ED8936', - 'ClearingRequested': '#D69E2E', - 'Absent': '#718096', - }; - return colors[status] || '#718096'; - }}; + border: 2px solid + ${({ status }) => { + const colors = { + Registered: '#48BB78', + RegistrationRequested: '#ED8936', + ClearingRequested: '#D69E2E', + Absent: '#718096', + } + return colors[status] || '#718096' + }}; background: transparent; &:after { @@ -171,15 +176,15 @@ const StatusSpan = styled.span<{ status: string; }>` border-radius: 50%; background-color: ${({ status }) => { const colors = { - 'Registered': '#48BB78', - 'RegistrationRequested': '#ED8936', - 'ClearingRequested': '#D69E2E', - 'Absent': '#718096', - }; - return colors[status] || '#718096'; + Registered: '#48BB78', + RegistrationRequested: '#ED8936', + ClearingRequested: '#D69E2E', + Absent: '#718096', + } + return colors[status] || '#718096' }}; } -`; +` const ItemHeader = styled.div` display: flex; @@ -190,7 +195,7 @@ const ItemHeader = styled.div` align-items: center; justify-content: space-between; margin-bottom: ${responsiveSize(32, 16)}; -`; +` const EntryDetailsContainer = styled.div` display: flex; @@ -213,7 +218,7 @@ const EntryDetailsContainer = styled.div` border: none; padding: 0; } -`; +` const LabelAndValue = styled.div` display: flex; @@ -221,11 +226,11 @@ const LabelAndValue = styled.div` gap: 8px; align-items: center; width: 100%; -`; +` const EvidenceSection = styled.div` gap: 16px; -`; +` const EvidenceSectionHeader = styled.div` display: flex; @@ -234,12 +239,12 @@ const EvidenceSectionHeader = styled.div` flex-wrap: wrap; gap: 8px; margin-bottom: 16px; -`; +` const EvidenceHeader = styled.h2` font-size: 20px; margin: 0; -`; +` const Evidence = styled.div` padding: 12px; @@ -249,7 +254,7 @@ const Evidence = styled.div` box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); margin-bottom: 16px; background: rgba(255, 255, 255, 0.05); -`; +` const EvidenceField = styled.div` margin: 8px 0; @@ -258,23 +263,23 @@ const EvidenceField = styled.div` flex-wrap: wrap; flex-direction: row; word-break: break-all; -`; +` const EvidenceDescription = styled(EvidenceField)` flex-direction: column; word-break: break-word; -`; +` const NoEvidenceText = styled.div` color: ${({ theme }) => theme.secondaryText}; font-style: italic; -`; +` const StyledReactMarkdown = styled(ReactMarkdown)` p { margin: 4px 0; } -`; +` const StyledButton = styled.button` height: fit-content; @@ -292,9 +297,9 @@ const StyledButton = styled.button` width: 16px; fill: ${({ theme }) => theme.primaryText}; } - ` + `, )} -`; +` export const StyledGitpodLink = styled.a` color: ${({ theme }) => theme.primaryText}; @@ -306,40 +311,52 @@ export const StyledGitpodLink = styled.a` &:hover { text-decoration: underline; } -`; +` const ItemDetails: React.FC = () => { - const { itemId } = useParams<{ itemId: string }>(); - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - const [currentTab, setCurrentTab] = useState(0); - const [isConfirmationOpen, setIsConfirmationOpen] = useState(false); - const [evidenceConfirmationType, setEvidenceConfirmationType] = useState(''); + const { itemId } = useParams<{ itemId: string }>() + const [searchParams, setSearchParams] = useSearchParams() + const navigate = useNavigate() + const [currentTab, setCurrentTab] = useState(0) + const [isConfirmationOpen, setIsConfirmationOpen] = useState(false) + const [evidenceConfirmationType, setEvidenceConfirmationType] = useState('') - const scrollTop = useScrollTop(); + const scrollTop = useScrollTop() const isAttachmentOpen = useMemo( () => !!searchParams.get('attachment'), - [searchParams] - ); + [searchParams], + ) const { isLoading: detailsLoading, data: detailsData } = useItemDetailsQuery({ itemId: itemId || '', enabled: !!itemId, - }); - - const registryParsedFromItemId = useMemo(() => itemId ? itemId.split('@')[1] : '', [itemId]); - - const challengePeriodDuration = useChallengePeriodDuration(registryParsedFromItemId); - const challengeRemainingTime = useChallengeRemainingTime(detailsData?.requests[0]?.submissionTime, detailsData?.disputed, challengePeriodDuration); - const formattedChallengeRemainingTime = useHumanizedCountdown(challengeRemainingTime, 2); - - const { data: countsData } = useItemCountsQuery(); + }) + + const registryParsedFromItemId = useMemo( + () => (itemId ? itemId.split('@')[1] : ''), + [itemId], + ) + + const challengePeriodDuration = useChallengePeriodDuration( + registryParsedFromItemId, + ) + const challengeRemainingTime = useChallengeRemainingTime( + detailsData?.requests[0]?.submissionTime, + detailsData?.disputed, + challengePeriodDuration, + ) + const formattedChallengeRemainingTime = useHumanizedCountdown( + challengeRemainingTime, + 2, + ) + + const { data: countsData } = useItemCountsQuery() const deposits = useMemo(() => { - if (!countsData) return undefined; - return countsData[revRegistryMap[registryParsedFromItemId]].deposits; - }, [countsData, registryParsedFromItemId]); + if (!countsData) return undefined + return countsData[revRegistryMap[registryParsedFromItemId]].deposits + }, [countsData, registryParsedFromItemId]) const { data: arbitrationCostData } = useQuery({ queryKey: [ @@ -350,67 +367,68 @@ const ItemDetails: React.FC = () => { queryFn: () => fetchArbitrationCost( detailsData?.requests?.[0].arbitrator || '', - detailsData?.requests?.[0].arbitratorExtraData || '' + detailsData?.requests?.[0].arbitratorExtraData || '', ), staleTime: Infinity, - }); + }) const evidences = useMemo(() => { - if (!detailsData) return []; + if (!detailsData) return [] return detailsData.requests .map((r) => r.evidenceGroup.evidences) .flat(1) - .sort((a, b) => Number(a.timestamp) - Number(b.timestamp)); - }, [detailsData]); + .sort((a, b) => Number(a.timestamp) - Number(b.timestamp)) + }, [detailsData]) const formattedDepositCost = useMemo(() => { - if (!detailsData || !deposits || arbitrationCostData === undefined) return '??? xDAI'; - let sum = 0n; + if (!detailsData || !deposits || arbitrationCostData === undefined) + return '??? xDAI' + let sum = 0n if (detailsData.status === 'Registered') { - sum = arbitrationCostData + deposits.removalBaseDeposit; + sum = arbitrationCostData + deposits.removalBaseDeposit } else if (detailsData.status === 'RegistrationRequested') { - sum = arbitrationCostData + deposits.submissionChallengeBaseDeposit; + sum = arbitrationCostData + deposits.submissionChallengeBaseDeposit } else if (detailsData.status === 'ClearingRequested') { - sum = arbitrationCostData + deposits.removalChallengeBaseDeposit; + sum = arbitrationCostData + deposits.removalChallengeBaseDeposit } - return `${Number(formatEther(sum))} xDAI`; - }, [detailsData, deposits, arbitrationCostData]); + return `${Number(formatEther(sum))} xDAI` + }, [detailsData, deposits, arbitrationCostData]) const AppealButton = () => { - if (!itemId) return null; - const [itemIdPart, contractAddress] = itemId.split('@'); - const redirectUrl = `https://curate.kleros.io/tcr/100/${contractAddress}/${itemIdPart}`; + if (!itemId) return null + const [itemIdPart, contractAddress] = itemId.split('@') + const redirectUrl = `https://curate.kleros.io/tcr/100/${contractAddress}/${itemIdPart}` return ( Appeal decision on Curate - ); - }; + ) + } const isTagsQueries = useMemo(() => { - const registry = searchParams.get('registry'); - return registry === 'Tags_Queries'; - }, [searchParams]); + const registry = searchParams.get('registry') + return registry === 'Tags_Queries' + }, [searchParams]) const getPropValue = (label: string) => { - return detailsData?.metadata?.props?.find((prop) => prop.label === label)?.value || ''; - }; + return detailsData?.props?.find((prop) => prop.label === label)?.value || '' + } const tabs = [ { key: 'details', label: 'Item Details' }, { key: 'evidence', label: 'Evidence' }, - ]; + ] const handleBackClick = () => { - navigate(-1); - }; + navigate(-1) + } if (detailsLoading || !detailsData) { return ( - ); + ) } return ( @@ -421,166 +439,207 @@ const ItemDetails: React.FC = () => { <> {isConfirmationOpen && ( )}
- - - Return - - - {detailsData?.metadata?.props?.find(p => p.label === 'Name')?.value || - detailsData?.metadata?.props?.find(p => p.label === 'Description')?.value || - 'Item Details'} - -
- - - {tabs.map((tab, i) => ( - setCurrentTab(i)}> - {tab.label} - - ))} - - - - {currentTab === 0 ? ( - // Item Details Tab - <> - - Entry Details - - {detailsData?.disputed ? 'Challenged' : getStatusLabel(detailsData.status)} - - {!detailsData.disputed ? ( - { - setIsConfirmationOpen(true); - setEvidenceConfirmationType(detailsData.status); - }} - status={detailsData.status} - > - {detailsData.status === 'Registered' && `Remove entry`} - {detailsData.status === 'RegistrationRequested' && 'Challenge entry'} - {detailsData.status === 'ClearingRequested' && 'Challenge removal'} - {' — ' + formattedDepositCost} - - ) : ( - - )} - - - {detailsData?.metadata?.props && !isTagsQueries && - detailsData?.metadata?.props.map(({ label, value }) => ( - - {label}: {renderValue(label, value)} - - ))} - {isTagsQueries && ( - <> - - Description: {getPropValue('Description')} - - - Github Repository URL: - - {getPropValue('Github Repository URL')} - - - Test on Gitpod - - - - Commit hash: {getPropValue('Commit hash')} - - - EVM Chain ID: {getPropValue('EVM Chain ID')} - - - )} - - Submitted by: - - - Submitted on: {formatTimestamp(Number(detailsData?.requests[0].submissionTime), true)} - - {detailsData?.status === "Registered" ? - - Included on: {formatTimestamp(Number(detailsData?.requests[0].resolutionTime), true)} - : null} - {formattedChallengeRemainingTime && ( - - Challenge Period ends in: {formattedChallengeRemainingTime} - - )} - - - ) : ( - // Evidence Tab - - - Evidence - { - setIsConfirmationOpen(true); - setEvidenceConfirmationType('Evidence'); - }} + + + Return + + + {detailsData?.props?.find((p) => p.label === 'Name')?.value || + detailsData?.props?.find((p) => p.label === 'Description') + ?.value || + 'Item Details'} + + + + + {tabs.map((tab, i) => ( + setCurrentTab(i)} > - Submit Evidence - - - - {evidences.length > 0 ? ( - evidences.map((evidence, idx) => ( - - - Title: {evidence?.metadata?.title} - - - Description: - {evidence?.metadata?.description || ''} - - {evidence?.metadata?.fileURI ? ( - + ))} + + + + {currentTab === 0 ? ( + // Item Details Tab + <> + + Entry Details + + {detailsData?.disputed + ? 'Challenged' + : getStatusLabel(detailsData.status)} + + {!detailsData.disputed ? ( + { - if (evidence.metadata?.fileURI) { - setSearchParams({ attachment: `https://cdn.kleros.link${evidence.metadata.fileURI}` }); - scrollTop(); - } + setIsConfirmationOpen(true) + setEvidenceConfirmationType(detailsData.status) }} + status={detailsData.status} > - - View Attached File - + {detailsData.status === 'Registered' && `Remove entry`} + {detailsData.status === 'RegistrationRequested' && + 'Challenge entry'} + {detailsData.status === 'ClearingRequested' && + 'Challenge removal'} + {' — ' + formattedDepositCost} + + ) : ( + + )} + + + {detailsData?.props && + !isTagsQueries && + detailsData?.props.map(({ label, value }) => ( + + {label}: {renderValue(label, value)} + + ))} + {isTagsQueries && ( + <> + + Description:{' '} + {getPropValue('Description')} + + + Github Repository URL: + + {getPropValue('Github Repository URL')} + + + Test on Gitpod + + + + Commit hash:{' '} + {getPropValue('Commit hash')} + + + EVM Chain ID:{' '} + {getPropValue('EVM Chain ID')}{' '} + + + + )} + + Submitted by:{' '} + + + + Submitted on:{' '} + {formatTimestamp( + Number(detailsData?.requests[0].submissionTime), + true, + )} + + {detailsData?.status === 'Registered' ? ( + + Included on:{' '} + {formatTimestamp( + Number(detailsData?.requests[0].resolutionTime), + true, + )} + ) : null} - - Time: {formatTimestamp(Number(evidence.timestamp), true)} - - - Party: {evidence.party} - - - )) + {formattedChallengeRemainingTime && ( + + Challenge Period ends in:{' '} + {formattedChallengeRemainingTime} + + )} + + ) : ( - No evidence submitted yet... + // Evidence Tab + + + Evidence + { + setIsConfirmationOpen(true) + setEvidenceConfirmationType('Evidence') + }} + > + Submit Evidence + + + + {evidences.length > 0 ? ( + evidences.map((evidence, idx) => ( + + + Title: {evidence?.title} + + + Description: + + {evidence?.description || ''} + + + {evidence?.fileURI ? ( + { + if (evidence?.fileURI) { + setSearchParams({ + attachment: `https://cdn.kleros.link${evidence.fileURI}`, + }) + scrollTop() + } + }} + > + + View Attached File + + ) : null} + + Time:{' '} + {formatTimestamp(Number(evidence.timestamp), true)} + + + Party: {evidence.party} + + + )) + ) : ( + No evidence submitted yet... + )} + )} - - )} - + )} - ); -}; + ) +} -export default ItemDetails; \ No newline at end of file +export default ItemDetails diff --git a/websites/app/src/pages/Registries/EntriesList/Entry.tsx b/websites/app/src/pages/Registries/EntriesList/Entry.tsx index e02b383..990d323 100644 --- a/websites/app/src/pages/Registries/EntriesList/Entry.tsx +++ b/websites/app/src/pages/Registries/EntriesList/Entry.tsx @@ -1,16 +1,21 @@ -import React, { useCallback, useState } from 'react'; -import styled from 'styled-components'; -import Skeleton from 'react-loading-skeleton'; -import { useSearchParams, useNavigate } from 'react-router-dom'; -import { formatEther } from 'ethers'; -import { GraphItem, registryMap } from 'utils/items'; -import { StyledWebsiteAnchor } from 'utils/renderValue'; -import AddressDisplay from 'components/AddressDisplay'; -import { useScrollTop } from 'hooks/useScrollTop'; -import { formatTimestamp } from 'utils/formatTimestamp'; -import useHumanizedCountdown, { useChallengeRemainingTime } from 'hooks/countdown'; -import { Divider } from 'components/Divider'; -import { hoverLongTransitionTiming, hoverShortTransitionTiming } from 'styles/commonStyles'; +import React, { useCallback, useState } from 'react' +import styled from 'styled-components' +import Skeleton from 'react-loading-skeleton' +import { useSearchParams, useNavigate } from 'react-router-dom' +import { formatEther } from 'ethers' +import { GraphItem, registryMap } from 'utils/items' +import { StyledWebsiteAnchor } from 'utils/renderValue' +import AddressDisplay from 'components/AddressDisplay' +import { useScrollTop } from 'hooks/useScrollTop' +import { formatTimestamp } from 'utils/formatTimestamp' +import useHumanizedCountdown, { + useChallengeRemainingTime, +} from 'hooks/countdown' +import { Divider } from 'components/Divider' +import { + hoverLongTransitionTiming, + hoverShortTransitionTiming, +} from 'styles/commonStyles' const Card = styled.div` color: white; @@ -23,9 +28,9 @@ const Card = styled.div` overflow: hidden; display: flex; flex-direction: column; -`; +` -const CardStatus = styled.div<{ status: string; }>` +const CardStatus = styled.div<{ status: string }>` text-align: center; font-weight: 400; padding: 14px 12px 12px; @@ -40,17 +45,17 @@ const CardStatus = styled.div<{ status: string; }>` height: 8px; margin-bottom: 0px; background-color: ${({ status }) => - ({ - Included: '#90EE90', - 'Registration Requested': '#FFEA00', - 'Challenged Submission': '#E87B35', - 'Challenged Removal': '#E87B35', - Removed: 'red', - }[status] || 'gray')}; + ({ + Included: '#90EE90', + 'Registration Requested': '#FFEA00', + 'Challenged Submission': '#E87B35', + 'Challenged Removal': '#E87B35', + Removed: 'red', + })[status] || 'gray'}; border-radius: 50%; margin-right: 10px; } -`; +` const CardContent = styled.div` flex: 1; @@ -70,7 +75,7 @@ const CardContent = styled.div` border-top-left-radius: 12px; border-top-right-radius: 12px; -`; +` const UpperCardContent = styled.div` display: flex; @@ -79,7 +84,7 @@ const UpperCardContent = styled.div` justify-content: center; align-items: center; padding: 0 16px; -`; +` const BottomCardContent = styled.div` display: flex; @@ -87,7 +92,7 @@ const BottomCardContent = styled.div` width: 100%; align-items: center; gap: 8px; -`; +` const TokenLogoWrapper = styled.div` ${hoverShortTransitionTiming} @@ -98,7 +103,7 @@ const TokenLogoWrapper = styled.div` &:hover { filter: brightness(0.8); } -`; +` const VisualProofWrapper = styled.img` ${hoverShortTransitionTiming} @@ -111,7 +116,7 @@ const VisualProofWrapper = styled.img` &:hover { filter: brightness(0.8); } -`; +` const DetailsButton = styled.button` ${hoverLongTransitionTiming} @@ -125,7 +130,9 @@ const DetailsButton = styled.button` background: transparent; border: none; cursor: pointer; - transition: transform 100ms ease-in-out, box-shadow 150ms ease-in-out; + transition: + transform 100ms ease-in-out, + box-shadow 150ms ease-in-out; &:before { content: ''; @@ -133,10 +140,12 @@ const DetailsButton = styled.button` inset: 0; padding: 1px; border-radius: 9999px; - background: linear-gradient(270deg, #1C3CF1 0%, #8B5CF6 100%); - -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + background: linear-gradient(270deg, #1c3cf1 0%, #8b5cf6 100%); + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); -webkit-mask-composite: xor; - mask-composite: exclude; + mask-composite: exclude; pointer-events: none; } @@ -148,14 +157,14 @@ const DetailsButton = styled.button` filter: brightness(1.2); transform: scale(1.03); } -`; +` const StyledButton = styled.button` cursor: pointer; background: none; border: none; padding: 0; -`; +` const LabelAndValue = styled.div` display: flex; @@ -163,102 +172,115 @@ const LabelAndValue = styled.div` gap: 8px; align-items: center; justify-content: center; -`; +` const ChainIdLabel = styled.label` margin-bottom: 8px; -`; +` const SymbolLabel = styled.label` color: ${({ theme }) => theme.primaryText}; font-weight: 600; font-size: 16px; margin-top: 4px; -`; +` const NameLabel = styled.label` color: ${({ theme }) => theme.secondaryText}; font-size: 16px; -`; +` const SubmittedLabel = styled.label` color: ${({ theme }) => theme.secondaryText}; font-size: 12px; -`; +` const StyledDivider = styled(Divider)` margin-bottom: 8px; -`; +` const TimersContainer = styled.div` display: flex; flex-direction: column; padding: 0 8px; gap: 4px; -`; +` const WrappedWebsiteContainer = styled.div` margin-top: -8px; -`; +` const readableStatusMap = { Registered: 'Included', Absent: 'Removed', RegistrationRequested: 'Registration Requested', ClearingRequested: 'Removal Requested', -}; +} const challengedStatusMap = { RegistrationRequested: 'Challenged Submission', ClearingRequested: 'Challenged Removal', -}; +} interface StatusProps { - status: 'Registered' | 'Absent' | 'RegistrationRequested' | 'ClearingRequested'; - disputed: boolean; - bounty: string; + status: + | 'Registered' + | 'Absent' + | 'RegistrationRequested' + | 'ClearingRequested' + disputed: boolean + bounty: string } const Status = React.memo(({ status, disputed, bounty }: StatusProps) => { const label = disputed ? challengedStatusMap[status] - : readableStatusMap[status]; + : readableStatusMap[status] const readableBounty = (status === 'ClearingRequested' || status === 'RegistrationRequested') && - !disputed + !disputed ? Number(formatEther(bounty)) - : null; + : null return ( {label} {readableBounty ? ` — $${readableBounty} 💰` : ''} - ); -}); + ) +}) const Entry = React.memo( - ({ item, challengePeriodDuration }: { item: GraphItem; challengePeriodDuration: number | null; }) => { - const [imgLoaded, setImgLoaded] = useState(false); - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - const scrollTop = useScrollTop(); + ({ + item, + challengePeriodDuration, + }: { + item: GraphItem + challengePeriodDuration: number | null + }) => { + const [imgLoaded, setImgLoaded] = useState(false) + const [searchParams, setSearchParams] = useSearchParams() + const navigate = useNavigate() + const scrollTop = useScrollTop() const challengeRemainingTime = useChallengeRemainingTime( item.requests[0]?.submissionTime, item.disputed, - challengePeriodDuration - ); - const formattedChallengeRemainingTime = useHumanizedCountdown(challengeRemainingTime, 2); + challengePeriodDuration, + ) + const formattedChallengeRemainingTime = useHumanizedCountdown( + challengeRemainingTime, + 2, + ) const handleEntryDetailsClick = useCallback(() => { - navigate(`/item/${item.id}?${searchParams.toString()}`); - }, [navigate, item.id, searchParams]); + navigate(`/item/${item.id}?${searchParams.toString()}`) + }, [navigate, item.id, searchParams]) const getPropValue = (label: string) => { - return item?.metadata?.props?.find((prop) => prop.label === label)?.value || ''; - }; + return item?.props?.find((prop) => prop.label === label)?.value || '' + } return ( @@ -272,8 +294,12 @@ const Entry = React.memo( {item.registryAddress === registryMap.Tags_Queries && ( <> - Chain: {getPropValue('EVM Chain ID')} - + + Chain: {getPropValue('EVM Chain ID')}{' '} + +
<>{getPropValue('Description')} @@ -311,9 +337,9 @@ const Entry = React.memo( {getPropValue('Logo') && ( { - const tokenLogoURI = `https://cdn.kleros.link${getPropValue('Logo')}`; - setSearchParams({ attachment: tokenLogoURI }); - scrollTop(); + const tokenLogoURI = `https://cdn.kleros.link${getPropValue('Logo')}` + setSearchParams({ attachment: tokenLogoURI }) + scrollTop() }} > @@ -329,13 +355,15 @@ const Entry = React.memo( )} {getPropValue('Symbol')} {getPropValue('Name')} - {getPropValue('Website') ? - {getPropValue('Website')} - : null} + {getPropValue('Website') ? ( + + {getPropValue('Website')} + + ) : null} )} {item.registryAddress === registryMap.CDN && ( @@ -347,16 +375,15 @@ const Entry = React.memo( target="_blank" rel="noopener noreferrer" > - {getPropValue('Domain name')} {getPropValue('Visual proof') && ( { - const visualProofURI = `https://cdn.kleros.link${getPropValue('Visual proof')}`; - setSearchParams({ attachment: visualProofURI }); - scrollTop(); + const visualProofURI = `https://cdn.kleros.link${getPropValue('Visual proof')}` + setSearchParams({ attachment: visualProofURI }) + scrollTop() }} > {!imgLoaded && } @@ -372,15 +399,29 @@ const Entry = React.memo( )} - Details + + Details + - {item?.status !== "Registered" ? - Submitted on: {formatTimestamp(Number(item?.requests[0].submissionTime), false)} - : null} - {item?.status === "Registered" ? - Included on: {formatTimestamp(Number(item?.requests[0].resolutionTime), false)} - : null} + {item?.status !== 'Registered' ? ( + + Submitted on:{' '} + {formatTimestamp( + Number(item?.requests[0].submissionTime), + false, + )} + + ) : null} + {item?.status === 'Registered' ? ( + + Included on:{' '} + {formatTimestamp( + Number(item?.requests[0].resolutionTime), + false, + )} + + ) : null} {formattedChallengeRemainingTime && ( Will be included in: {formattedChallengeRemainingTime} @@ -390,8 +431,8 @@ const Entry = React.memo( - ); - } -); + ) + }, +) -export default Entry; \ No newline at end of file +export default Entry diff --git a/websites/app/src/pages/Registries/ExportModal.tsx b/websites/app/src/pages/Registries/ExportModal.tsx index 55ecaa9..ef7916d 100644 --- a/websites/app/src/pages/Registries/ExportModal.tsx +++ b/websites/app/src/pages/Registries/ExportModal.tsx @@ -1,25 +1,25 @@ -import React, { useEffect, useRef, useState } from "react"; -import styled, { css } from "styled-components"; -import { landscapeStyle } from 'styles/landscapeStyle'; -import { useExportItems, ExportFilters } from "hooks/queries/useExportItems"; -import { json2csv } from "json-2-csv"; -import { revRegistryMap } from 'utils/items'; -import { chains } from "utils/chains"; -import ExportIcon from "svgs/icons/export.svg"; - -import EthereumIcon from 'svgs/chains/ethereum.svg'; -import SolanaIcon from 'svgs/chains/solana.svg'; -import BaseIcon from 'svgs/chains/base.svg'; -import CeloIcon from 'svgs/chains/celo.svg'; -import ScrollIcon from 'svgs/chains/scroll.svg'; -import FantomIcon from 'svgs/chains/fantom.svg'; -import ZkSyncIcon from 'svgs/chains/zksync.svg'; -import GnosisIcon from 'svgs/chains/gnosis.svg'; -import PolygonIcon from 'svgs/chains/polygon.svg'; -import OptimismIcon from 'svgs/chains/optimism.svg'; -import ArbitrumIcon from 'svgs/chains/arbitrum.svg'; -import AvalancheIcon from 'svgs/chains/avalanche.svg'; -import BnbIcon from 'svgs/chains/bnb.svg'; +import React, { useEffect, useRef, useState } from 'react' +import styled, { css } from 'styled-components' +import { landscapeStyle } from 'styles/landscapeStyle' +import { useExportItems, ExportFilters } from 'hooks/queries/useExportItems' +import { json2csv } from 'json-2-csv' +import { revRegistryMap } from 'utils/items' +import { chains } from 'utils/chains' +import ExportIcon from 'svgs/icons/export.svg' + +import EthereumIcon from 'svgs/chains/ethereum.svg' +import SolanaIcon from 'svgs/chains/solana.svg' +import BaseIcon from 'svgs/chains/base.svg' +import CeloIcon from 'svgs/chains/celo.svg' +import ScrollIcon from 'svgs/chains/scroll.svg' +import FantomIcon from 'svgs/chains/fantom.svg' +import ZkSyncIcon from 'svgs/chains/zksync.svg' +import GnosisIcon from 'svgs/chains/gnosis.svg' +import PolygonIcon from 'svgs/chains/polygon.svg' +import OptimismIcon from 'svgs/chains/optimism.svg' +import ArbitrumIcon from 'svgs/chains/arbitrum.svg' +import AvalancheIcon from 'svgs/chains/avalanche.svg' +import BnbIcon from 'svgs/chains/bnb.svg' const ModalOverlay = styled.div` position: fixed; @@ -32,7 +32,7 @@ const ModalOverlay = styled.div` justify-content: center; align-items: center; z-index: 50; -`; +` const ModalWrapper = styled.div` position: relative; @@ -47,10 +47,12 @@ const ModalWrapper = styled.div` inset: 0; padding: 1px; border-radius: 20px; - background: linear-gradient(180deg, #7186FF90 0%, #BEBEC590 100%); - -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + background: linear-gradient(180deg, #7186ff90 0%, #bebec590 100%); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); -webkit-mask-composite: xor; - mask-composite: exclude; + mask-composite: exclude; pointer-events: none; } @@ -58,9 +60,9 @@ const ModalWrapper = styled.div` () => css` width: 60%; max-width: 700px; - ` + `, )} -`; +` const ModalContainer = styled.div` background: linear-gradient( @@ -81,9 +83,7 @@ const ModalContainer = styled.div` gap: 24px; position: relative; box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.4); -`; - - +` const ModalHeader = styled.div` display: flex; @@ -91,26 +91,26 @@ const ModalHeader = styled.div` align-items: center; border-bottom: 1px solid rgba(113, 134, 255, 0.3); padding-bottom: 20px; -`; +` const ModalTitle = styled.h2` color: ${({ theme }) => theme.primaryText}; font-size: 20px; font-weight: 600; margin: 0; -`; +` const ModalSubtitle = styled.div` font-size: 14px; color: ${({ theme }) => theme.secondaryText}; margin-top: 4px; line-height: 1.4; -`; +` const HeaderContent = styled.div` flex: 1; min-width: 0; -`; +` const CloseButton = styled.button` background: none; @@ -124,27 +124,26 @@ const CloseButton = styled.button` &:hover { color: ${({ theme }) => theme.primaryText}; } -`; - +` const FilterSection = styled.div` display: flex; flex-direction: column; gap: 16px; -`; +` const FilterGroup = styled.div` display: flex; flex-direction: column; gap: 8px; -`; +` const GroupHeader = styled.div` display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; -`; +` const FilterGroupTitle = styled.h4` color: ${({ theme }) => theme.accent}; @@ -161,21 +160,20 @@ const FilterGroupTitle = styled.h4` height: 14px; fill: ${({ theme }) => theme.accent}; } -`; - +` const CheckboxGroup = styled.div` display: flex; flex-direction: column; gap: 8px; -`; +` const NetworkGrid = styled.div` display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px; margin-top: 8px; -`; +` const CheckboxItem = styled.div` display: flex; @@ -193,7 +191,7 @@ const CheckboxItem = styled.div` opacity: 1; } } -`; +` const CheckboxLabel = styled.label` display: flex; @@ -201,7 +199,7 @@ const CheckboxLabel = styled.label` gap: 8px; cursor: pointer; flex: 1; -`; +` const NetworkItem = styled.div` display: flex; @@ -221,7 +219,7 @@ const NetworkItem = styled.div` opacity: 1; } } -`; +` const NetworkLabel = styled.label` display: flex; @@ -235,14 +233,13 @@ const NetworkLabel = styled.label` height: 16px; flex-shrink: 0; } -`; - +` const Checkbox = styled.input.attrs({ type: 'checkbox' })` width: 16px; height: 16px; accent-color: ${({ theme }) => theme.accent}; -`; +` const OnlyButton = styled.button` background: none; @@ -259,7 +256,7 @@ const OnlyButton = styled.button` &:hover { background: ${({ theme }) => theme.lightGrey}; } -`; +` const ActionButton = styled.button` background: none; @@ -277,7 +274,7 @@ const ActionButton = styled.button` background: ${({ theme }) => theme.lightGrey}; opacity: 1; } -`; +` const DateInput = styled.input` background: rgba(26, 11, 46, 0.3); @@ -297,7 +294,7 @@ const DateInput = styled.input` &::-webkit-calendar-picker-indicator { filter: invert(1); } -`; +` const FooterButtons = styled.div` display: flex; @@ -305,7 +302,7 @@ const FooterButtons = styled.div` gap: 12px; padding-top: 20px; border-top: 1px solid rgba(113, 134, 255, 0.3); -`; +` const CancelButton = styled.button` padding: 12px 24px; @@ -323,7 +320,7 @@ const CancelButton = styled.button` background: ${({ theme }) => theme.lightGrey}; color: ${({ theme }) => theme.primaryText}; } -`; +` const ExportButton = styled.button<{ disabled: boolean }>` padding: 12px 24px; @@ -336,32 +333,33 @@ const ExportButton = styled.button<{ disabled: boolean }>` display: flex; align-items: center; gap: 8px; - background: linear-gradient(135deg, #7186FF 0%, #9B59B6 100%); + background: linear-gradient(135deg, #7186ff 0%, #9b59b6 100%); color: white; border: none; box-shadow: 0 4px 16px rgba(113, 134, 255, 0.3); - opacity: ${({ disabled }) => disabled ? 0.7 : 1}; - cursor: ${({ disabled }) => disabled ? 'not-allowed' : 'pointer'}; + opacity: ${({ disabled }) => (disabled ? 0.7 : 1)}; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; &:hover { - box-shadow: ${({ disabled }) => disabled ? '0 4px 16px rgba(113, 134, 255, 0.3)' : '0 6px 20px rgba(113, 134, 255, 0.4)'}; - filter: ${({ disabled }) => disabled ? 'none' : 'brightness(1.05)'}; + box-shadow: ${({ disabled }) => + disabled + ? '0 4px 16px rgba(113, 134, 255, 0.3)' + : '0 6px 20px rgba(113, 134, 255, 0.4)'}; + filter: ${({ disabled }) => (disabled ? 'none' : 'brightness(1.05)')}; } svg { width: 16px; height: 16px; } -`; - - +` const STATUS_OPTIONS = [ { value: 'Registered', label: 'Registered' }, { value: 'RegistrationRequested', label: 'Registration Requested' }, { value: 'ClearingRequested', label: 'Removal Requested' }, - { value: 'Absent', label: 'Removed' } -]; + { value: 'Absent', label: 'Removed' }, +] const getChainIcon = (chainId: string) => { const iconMap = { @@ -378,28 +376,33 @@ const getChainIcon = (chainId: string) => { '42161': ArbitrumIcon, '43114': AvalancheIcon, '56': BnbIcon, - }; - return iconMap[chainId] || null; -}; + } + return iconMap[chainId] || null +} const NETWORK_OPTIONS = chains .filter((chain) => !chain.deprecated) - .map((chain) => ({ value: chain.id, label: chain.name })); + .map((chain) => ({ value: chain.id, label: chain.name })) interface ExportModalProps { - registryAddress: string; - onClose: () => void; + registryAddress: string + onClose: () => void } -const ExportModal: React.FC = ({ registryAddress, onClose }) => { - const [hasClickedExport, setHasClickedExport] = useState(false); - const [isButtonLoading, setIsButtonLoading] = useState(false); - const ref = useRef(null); - - const [selectedStatuses, setSelectedStatuses] = useState(['Registered']); - const [selectedNetworks, setSelectedNetworks] = useState([]); - const [fromDate, setFromDate] = useState(''); - const [toDate, setToDate] = useState(''); +const ExportModal: React.FC = ({ + registryAddress, + onClose, +}) => { + const [hasClickedExport, setHasClickedExport] = useState(false) + const [isButtonLoading, setIsButtonLoading] = useState(false) + const ref = useRef(null) + + const [selectedStatuses, setSelectedStatuses] = useState([ + 'Registered', + ]) + const [selectedNetworks, setSelectedNetworks] = useState([]) + const [fromDate, setFromDate] = useState('') + const [toDate, setToDate] = useState('') const exportFilters: ExportFilters = { registryId: registryAddress, @@ -407,50 +410,55 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = network: selectedNetworks.length > 0 ? selectedNetworks : undefined, fromDate: fromDate || undefined, toDate: toDate || undefined, - }; + } - const { data: items, refetch, isRefetching } = useExportItems(exportFilters); + const { data: items, refetch, isRefetching } = useExportItems(exportFilters) const handleStatusChange = (status: string, checked: boolean) => { if (checked) { - setSelectedStatuses(prev => [...prev, status]); + setSelectedStatuses((prev) => [...prev, status]) } else { - setSelectedStatuses(prev => prev.filter(s => s !== status)); + setSelectedStatuses((prev) => prev.filter((s) => s !== status)) } - }; + } const handleNetworkChange = (network: string, checked: boolean) => { if (checked) { - setSelectedNetworks(prev => [...prev, network]); + setSelectedNetworks((prev) => [...prev, network]) } else { - setSelectedNetworks(prev => prev.filter(n => n !== network)); + setSelectedNetworks((prev) => prev.filter((n) => n !== network)) } - }; + } const handleNetworkOnly = (selectedNetwork: string) => { - setSelectedNetworks([selectedNetwork]); - }; + setSelectedNetworks([selectedNetwork]) + } const handleNetworkAll = () => { - setSelectedNetworks([]); - }; + setSelectedNetworks([]) + } const handleStatusOnly = (selectedStatus: string) => { - setSelectedStatuses([selectedStatus]); - }; + setSelectedStatuses([selectedStatus]) + } const handleStatusAll = () => { - setSelectedStatuses(['Registered', 'RegistrationRequested', 'ClearingRequested', 'Absent']); - }; + setSelectedStatuses([ + 'Registered', + 'RegistrationRequested', + 'ClearingRequested', + 'Absent', + ]) + } const handleExport = () => { - setIsButtonLoading(true); - setHasClickedExport(true); - refetch(); - }; + setIsButtonLoading(true) + setHasClickedExport(true) + refetch() + } useEffect(() => { - if (!items || !ref.current || !hasClickedExport) return; + if (!items || !ref.current || !hasClickedExport) return try { const flattenedItems = items.map((item) => { @@ -458,54 +466,69 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = id: item.id, status: item.status, disputed: item.disputed, - submissionTime: new Date(parseInt(item.latestRequestSubmissionTime) * 1000).toISOString(), + submissionTime: new Date( + parseInt(item.latestRequestSubmissionTime) * 1000, + ).toISOString(), registryAddress: item.registryAddress, itemID: item.itemID, - }; + } - if (item.metadata?.props) { - item.metadata.props.forEach((prop) => { - row[`${prop.label} (${prop.description})`] = prop.value; - }); + if (item?.props) { + item.props.forEach((prop) => { + row[`${prop.label} (${prop.description})`] = prop.value + }) } - if (item.metadata) { - row.key0 = item.metadata.key0; - row.key1 = item.metadata.key1; - row.key2 = item.metadata.key2; - row.key3 = item.metadata.key3; - row.key4 = item.metadata.key4; + if (item) { + row.key0 = item.key0 + row.key1 = item.key1 + row.key2 = item.key2 + row.key3 = item.key3 + row.key4 = item.key4 } - return row; - }); + return row + }) - const csvData = json2csv(flattenedItems); - const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" }); - const link = ref.current; + const csvData = json2csv(flattenedItems) + const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' }) + const link = ref.current if (link) { - const url = URL.createObjectURL(blob); - link.setAttribute("href", url); - const registryName = revRegistryMap[registryAddress] || 'Items'; - const dateRange = fromDate || toDate ? `_${fromDate || 'start'}-${toDate || 'end'}` : ''; - const statusSuffix = selectedStatuses.length === 1 ? `_${selectedStatuses[0]}` : ''; - link.setAttribute("download", `Kleros-Curate-${registryName}${statusSuffix}${dateRange}.csv`); - link.click(); - URL.revokeObjectURL(url); + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + const registryName = revRegistryMap[registryAddress] || 'Items' + const dateRange = + fromDate || toDate ? `_${fromDate || 'start'}-${toDate || 'end'}` : '' + const statusSuffix = + selectedStatuses.length === 1 ? `_${selectedStatuses[0]}` : '' + link.setAttribute( + 'download', + `Kleros-Curate-${registryName}${statusSuffix}${dateRange}.csv`, + ) + link.click() + URL.revokeObjectURL(url) // Close modal after successful export - setHasClickedExport(false); // Reset for next time - onClose(); + setHasClickedExport(false) // Reset for next time + onClose() } } catch (error) { - console.error('Error preparing CSV:', error); + console.error('Error preparing CSV:', error) } finally { - setIsButtonLoading(false); + setIsButtonLoading(false) } - }, [items, registryAddress, selectedStatuses, fromDate, toDate, onClose, hasClickedExport]); - - const canExport = selectedStatuses.length > 0; + }, [ + items, + registryAddress, + selectedStatuses, + fromDate, + toDate, + onClose, + hasClickedExport, + ]) + + const canExport = selectedStatuses.length > 0 return ( @@ -524,10 +547,10 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = - Status (select at least one) - - All - + + Status (select at least one) + + All {STATUS_OPTIONS.map((option) => ( @@ -535,7 +558,9 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = handleStatusChange(option.value, e.target.checked)} + onChange={(e) => + handleStatusChange(option.value, e.target.checked) + } /> {option.label} @@ -554,20 +579,22 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = - Networks (leave empty for all) - - All - + + Networks (leave empty for all) + + All {NETWORK_OPTIONS.map((option) => { - const ChainIcon = getChainIcon(option.value); + const ChainIcon = getChainIcon(option.value) return ( handleNetworkChange(option.value, e.target.checked)} + onChange={(e) => + handleNetworkChange(option.value, e.target.checked) + } /> {ChainIcon && } {option.label} @@ -580,7 +607,7 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = Only - ); + ) })} @@ -588,7 +615,9 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = - From Date (leave empty for all history) + + From Date (leave empty for all history) + = ({ registryAddress, onClose }) = - To Date (leave empty for current date) + + To Date (leave empty for current date) + = ({ registryAddress, onClose }) = onClick={handleExport} > {isButtonLoading ? ( - isRefetching ? "Fetching data..." : "Preparing CSV..." + isRefetching ? ( + 'Fetching data...' + ) : ( + 'Preparing CSV...' + ) ) : ( <> Export CSV @@ -626,11 +661,19 @@ const ExportModal: React.FC = ({ registryAddress, onClose }) = )} - + - ); -}; + ) +} -export default ExportModal; \ No newline at end of file +export default ExportModal diff --git a/websites/app/src/utils/itemCounts.ts b/websites/app/src/utils/itemCounts.ts index 89ab30d..7e55b05 100644 --- a/websites/app/src/utils/itemCounts.ts +++ b/websites/app/src/utils/itemCounts.ts @@ -7,7 +7,6 @@ export interface RegistryMetadata { policyURI: string logoURI: string } - export interface FocusedRegistry { numberOfAbsent: number numberOfRegistered: number diff --git a/websites/app/src/utils/itemDetails.ts b/websites/app/src/utils/itemDetails.ts index 5c84d32..d61b418 100644 --- a/websites/app/src/utils/itemDetails.ts +++ b/websites/app/src/utils/itemDetails.ts @@ -6,12 +6,10 @@ export interface GraphEvidence { number: string timestamp: string txHash: string - metadata: { - title: string | null - description: string | null - fileURI: string | null - fileTypeExtension: string | null - } | null + title: string | null + description: string | null + fileURI: string | null + fileTypeExtension: string | null } export interface EvidenceGroup { @@ -40,12 +38,10 @@ export interface GraphItemDetails { | 'RegistrationRequested' | 'ClearingRequested' disputed: boolean - metadata: { - key0: string - key1: string - key2: string - key3: string - props: Prop[] - } | null + key0: string + key1: string + key2: string + key3: string + props: Prop[] requests: RequestDetails[] } diff --git a/websites/app/src/utils/items.ts b/websites/app/src/utils/items.ts index 584b4c7..1065094 100644 --- a/websites/app/src/utils/items.ts +++ b/websites/app/src/utils/items.ts @@ -1,64 +1,64 @@ - export const registryMap = { Single_Tags: '0x66260c69d03837016d88c9877e61e08ef74c59f2', Tags_Queries: '0xae6aaed5434244be3699c56e7ebc828194f26dc3', CDN: '0x957a53a994860be4750810131d9c876b2f52d6e1', Tokens: '0xee1502e29795ef6c2d60f8d7120596abe3bad990', -}; +} export const revRegistryMap = { '0x66260c69d03837016d88c9877e61e08ef74c59f2': 'Single_Tags', '0xae6aaed5434244be3699c56e7ebc828194f26dc3': 'Tags_Queries', '0x957a53a994860be4750810131d9c876b2f52d6e1': 'CDN', '0xee1502e29795ef6c2d60f8d7120596abe3bad990': 'Tokens', -}; +} export interface GraphItem { - id: string; - latestRequestSubmissionTime: string; - registryAddress: string; - itemID: string; - status: 'Registered' | 'Absent' | 'RegistrationRequested' | 'ClearingRequested'; - disputed: boolean; - data: string; - metadata: { - key0: string; - key1: string; - key2: string; - key3: string; - key4: string; - props: Prop[]; - } | null; - requests: Request[]; + id: string + latestRequestSubmissionTime: string + registryAddress: string + itemID: string + status: + | 'Registered' + | 'Absent' + | 'RegistrationRequested' + | 'ClearingRequested' + disputed: boolean + data: string + key0: string + key1: string + key2: string + key3: string + key4: string + props: Prop[] + requests: Request[] } export interface Prop { - value: string; - type: string; - label: string; - description: string; - isIdentifier: boolean; + value: string + type: string + label: string + description: string + isIdentifier: boolean } export interface Request { - disputed: boolean; - disputeID: string; - submissionTime: string; - resolved: boolean; - requester: string; - challenger: string; - resolutionTime: string; - deposit: string; - rounds: Round[]; + disputed: boolean + disputeID: string + submissionTime: string + resolved: boolean + requester: string + challenger: string + resolutionTime: string + deposit: string + rounds: Round[] } export interface Round { - appealPeriodStart: string; - appealPeriodEnd: string; - ruling: string; - hasPaidRequester: boolean; - hasPaidChallenger: boolean; - amountPaidRequester: string; - amountPaidChallenger: string; + appealPeriodStart: string + appealPeriodEnd: string + ruling: string + hasPaidRequester: boolean + hasPaidChallenger: boolean + amountPaidRequester: string + amountPaidChallenger: string } - diff --git a/websites/app/src/utils/validateAddress.ts b/websites/app/src/utils/validateAddress.ts index 3e5d67d..0f15674 100644 --- a/websites/app/src/utils/validateAddress.ts +++ b/websites/app/src/utils/validateAddress.ts @@ -1,7 +1,7 @@ import { isAddress } from 'ethers' import request, { gql } from 'graphql-request' import { registryMap } from './items' -import { SUBGRAPH_GNOSIS_ENDPOINT } from 'consts/index'; +import { SUBGRAPH_GNOSIS_ENDPOINT } from 'consts/index' import { PublicKey } from '@solana/web3.js' import { chains } from 'utils/chains' import bs58check from 'bs58check' @@ -48,7 +48,9 @@ const isBip122Address = (value: string): boolean => { } const isValidAddressForChain = (chainId: string, address: string): boolean => { - const network = chains.find(chain => `${chain.namespace}:${chain.id}` === chainId) + const network = chains.find( + (chain) => `${chain.namespace}:${chain.id}` === chainId, + ) if (!network) return false if (network.namespace === 'solana') return isSolanaAddress(address) @@ -62,49 +64,48 @@ const isValidAddressForChain = (chainId: string, address: string): boolean => { export interface Issue { address?: { - severity: 'warn' | 'error'; - message: string; - }; + severity: 'warn' | 'error' + message: string + } domain?: { - severity: 'warn' | 'error'; - message: string; - }; + severity: 'warn' | 'error' + message: string + } contract?: { - severity: 'warn' | 'error'; - message: string; - }; + severity: 'warn' | 'error' + message: string + } projectName?: { - severity: 'warn' | 'error'; - message: string; - }; + severity: 'warn' | 'error' + message: string + } publicNameTag?: { - severity: 'warn' | 'error'; - message: string; - }; + severity: 'warn' | 'error' + message: string + } link?: { - severity: 'warn' | 'error'; - message: string; - }; + severity: 'warn' | 'error' + message: string + } symbol?: { - severity: 'warn' | 'error'; - message: string; - }; + severity: 'warn' | 'error' + message: string + } } const getDupesInRegistry = async ( richAddress: string, registryAddress: string, - domain?: string + domain?: string, ): Promise => { const query = gql` - query ($registry: String!, $richAddress: String!) { - litems( + query ($registry: String!, $richAddress: String!, $domain: String) { + litems: LItem( where: { - registry: $registry, - status_in: ["Registered", "ClearingRequested", "RegistrationRequested"], - metadata_ : { key0_contains_nocase: $richAddress, - ${domain ? `key1_starts_with_nocase: "${domain}"` : ''} - }, + registry_id: { _eq: $registry } + status: { _in: [ "Registered", "ClearingRequested", "RegistrationRequested"] } + key0: { _ilike: $richAddress } + ${domain ? `key1: { _ilike: $domain }` : ''} } ) { id @@ -112,13 +113,18 @@ const getDupesInRegistry = async ( } ` + const variables: Record = { + registry: registryAddress, + richAddress: `%${richAddress}%`, // contains + } + if (domain) { + variables.domain = `${domain}%` // starts with + } + const result = (await request({ url: SUBGRAPH_GNOSIS_ENDPOINT, document: query, - variables: { - registry: registryAddress, - richAddress, - }, + variables, })) as any const items = result.litems return items.length @@ -126,21 +132,21 @@ const getDupesInRegistry = async ( const getTokenDupesWithWebsiteCheck = async ( richAddress: string, - registryAddress: string + registryAddress: string, ): Promise => { const query = gql` query ($registry: String!, $richAddress: String!) { - litems( + litems: LItem( where: { - registry: $registry, - status_in: ["Registered", "ClearingRequested", "RegistrationRequested"], - metadata_ : { key0_contains_nocase: $richAddress }, + registry_id: { _eq: $registry } + status: { + _in: ["Registered", "ClearingRequested", "RegistrationRequested"] + } + key0: { _ilike: $richAddress } } ) { id - metadata { - key3 - } + key3 } } ` @@ -153,12 +159,12 @@ const getTokenDupesWithWebsiteCheck = async ( richAddress, }, })) as any - + // Only count duplicates if existing entries have a website (key3) - const duplicatesWithWebsite = result.litems.filter((item: any) => - item.metadata?.key3 && item.metadata.key3.trim() !== '' + const duplicatesWithWebsite = result.litems.filter( + (item: any) => item?.key3 && item.key3.trim() !== '', ) - + return duplicatesWithWebsite.length } @@ -170,72 +176,110 @@ export const getAddressValidationIssue = async ( projectName?: string, publicNameTag?: string, link?: string, - symbol?: string + symbol?: string, ): Promise => { const result: Issue = {} if (address && !isValidAddressForChain(chainId, address)) { - const network = chains.find(chain => `${chain.namespace}:${chain.id}` === chainId) + const network = chains.find( + (chain) => `${chain.namespace}:${chain.id}` === chainId, + ) let message = 'Invalid address for the specified chain' - + if (network?.namespace === 'eip155' && !address.startsWith('0x')) { message = 'Address must start with "0x" prefix for Ethereum-like chains' - } else if (network?.namespace === 'eip155' && address.startsWith('0x') && !isAddress(address)) { + } else if ( + network?.namespace === 'eip155' && + address.startsWith('0x') && + !isAddress(address) + ) { message = 'Invalid Ethereum address format' } - + result.address = { message, severity: 'error' } } if (publicNameTag && publicNameTag.length > 50) { - result.publicNameTag = { message: 'Public Name Tag too long (max 50 characters)', severity: 'error' } + result.publicNameTag = { + message: 'Public Name Tag too long (max 50 characters)', + severity: 'error', + } } if (projectName && projectName !== projectName.trim()) { - result.projectName = { message: 'Project name has leading or trailing whitespace', severity: 'warn' } + result.projectName = { + message: 'Project name has leading or trailing whitespace', + severity: 'warn', + } } if (registry === 'Tokens' && projectName && projectName.length > 40) { - result.projectName = { message: 'Public Name too long (max 40 characters)', severity: 'warn' } + result.projectName = { + message: 'Public Name too long (max 40 characters)', + severity: 'warn', + } } if (registry === 'Tokens' && symbol && symbol.length > 20) { - result.symbol = { message: 'Symbol too long (max 20 characters)', severity: 'warn' } + result.symbol = { + message: 'Symbol too long (max 20 characters)', + severity: 'warn', + } } if (publicNameTag && publicNameTag !== publicNameTag.trim()) { - result.publicNameTag = { message: 'Public Name Tag has leading or trailing whitespace', severity: 'warn' } + result.publicNameTag = { + message: 'Public Name Tag has leading or trailing whitespace', + severity: 'warn', + } } const cdnRegex = /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/\S*)?$/ if (domain && !cdnRegex.test(domain)) { - result.domain = { message: 'Invalid website format for CDN. Must be a valid domain', severity: 'error' } + result.domain = { + message: 'Invalid website format for CDN. Must be a valid domain', + severity: 'error', + } } const tagRegex = /^https?:\/\/([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/\S*)?$/ if (link && !tagRegex.test(link)) { - result.link = { message: 'Invalid website format. Must start with http(s):// and include a valid domain', severity: 'error' } + result.link = { + message: + 'Invalid website format. Must start with http(s):// and include a valid domain', + severity: 'error', + } } if (Object.keys(result).length > 0) return result // Check for duplicates based on registry type if (registry === 'Single_Tags' || registry === 'CDN') { - const ndupes = await getDupesInRegistry(chainId + ':' + address, registryMap[registry], domain) + const ndupes = await getDupesInRegistry( + chainId + ':' + address, + registryMap[registry], + domain, + ) if (ndupes > 0) { result.domain = { message: 'Duplicate submission', severity: 'error' } } } else if (registry === 'Tokens') { // For tokens, only consider it a duplicate if any of the existing entries have a website - const ndupes = await getTokenDupesWithWebsiteCheck(chainId + ':' + address, registryMap[registry]) + const ndupes = await getTokenDupesWithWebsiteCheck( + chainId + ':' + address, + registryMap[registry], + ) if (ndupes > 0) { - result.domain = { message: 'Duplicate submission - token with website already exists', severity: 'error' } + result.domain = { + message: 'Duplicate submission - token with website already exists', + severity: 'error', + } } } - return Object.keys(result).length > 0 ? result : null; + return Object.keys(result).length > 0 ? result : null } export default getAddressValidationIssue