Skip to content

Commit 5c42800

Browse files
authored
Merge pull request #345 from kleros/feat/add-more-info-seer-registry
feat: fix some bugs, and add extra details for seer entries
2 parents 888fd32 + 135315d commit 5c42800

File tree

11 files changed

+439
-12
lines changed

11 files changed

+439
-12
lines changed

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,12 @@ REACT_APP_RPC_URLS={"100":"https://rpc.gnosischain.com","1":"https://mainnet.inf
2020
REACT_APP_FORMATIC_API_KEYS=
2121

2222
# If provided, the welcome modal display the following video.
23-
REACT_APP_INSTRUCTION_VIDEO=https://www.youtube.com/embed/DKPVWzhh8Y8
23+
REACT_APP_INSTRUCTION_VIDEO=https://www.youtube.com/embed/DKPVWzhh8Y8
24+
25+
REACT_APP_SUBGRAPH_MAINNET=https://api.studio.thegraph.com/query/61738/legacy-curate-mainnet/version/latest
26+
REACT_APP_SUBGRAPH_GNOSIS=https://api.studio.thegraph.com/query/61738/legacy-curate-gnosis/version/latest
27+
REACT_APP_SUBGRAPH_SEPOLIA=https://api.studio.thegraph.com/query/61738/legacy-curate-sepolia/version/latest
28+
29+
# For the Seer registries in Mainnet and Gnosis.
30+
REACT_APP_SEER_SUBGRAPH_MAINNET=https://gateway.thegraph.com/api/[api-key]/subgraphs/id/BMQD869m8LnGJJfqMRjcQ16RTyUw6EUx5jkh3qWhSn3M
31+
REACT_APP_SEER_SUBGRAPH_GNOSIS=https://gateway.thegraph.com/api/[api-key]/subgraphs/id/B4vyRqJaSHD8dRDb3BFRoAzuBK18c1QQcXq94JbxDxWH
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { seerAddresses } from 'config/tcr-addresses'
2+
3+
export const isSeerRegistry = (tcrAddress: string, chainId: string) =>
4+
seerAddresses[chainId as keyof typeof seerAddresses] ===
5+
tcrAddress.toLowerCase()
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React from 'react'
2+
import styled from 'styled-components'
3+
4+
interface ISeerCardContent {
5+
chainId: string
6+
contractAddress: string
7+
marketName?: string
8+
outcomes?: string[]
9+
}
10+
11+
const Container = styled.div`
12+
font-family: 'Arial';
13+
max-width: 300px;
14+
margin: 16px auto;
15+
padding: 10px;
16+
border: 1px solid #e0e0e0;
17+
border-radius: 8px;
18+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
19+
`
20+
21+
const SeerLink = styled.a`
22+
color: #007bff;
23+
text-decoration: none;
24+
font-weight: bold;
25+
font-size: 14px;
26+
27+
&:hover,
28+
&:focus {
29+
text-decoration: underline;
30+
}
31+
`
32+
33+
const MarketName = styled.h3`
34+
margin: 0 0 12px;
35+
font-size: 1.2em;
36+
color: #333;
37+
`
38+
39+
const OutcomesHeading = styled.h4`
40+
margin: 0 0 12px;
41+
font-size: 0.9em;
42+
color: #666;
43+
`
44+
45+
const OutcomeItem = styled.div`
46+
display: flex;
47+
align-items: center;
48+
margin-bottom: 6px;
49+
padding: 4px;
50+
background-color: #f9f9f9;
51+
border-radius: 4px;
52+
`
53+
54+
const OutcomeName = styled.span`
55+
font-size: 0.9em;
56+
color: #333;
57+
`
58+
59+
const LoadingMessage = styled.p`
60+
color: #666;
61+
font-size: 12px;
62+
`
63+
64+
const SeerCardContent: React.FC<ISeerCardContent> = ({
65+
chainId,
66+
contractAddress,
67+
marketName,
68+
outcomes
69+
}) => {
70+
const filteredOutcomes = outcomes?.filter(
71+
(outcome: string) => outcome !== 'Invalid result'
72+
)
73+
74+
if (!marketName)
75+
return <LoadingMessage>Loading Seer details...</LoadingMessage>
76+
77+
return (
78+
<Container>
79+
<p>
80+
<SeerLink
81+
href={`https://app.seer.pm/markets/${chainId}/${contractAddress}`}
82+
target="_blank"
83+
rel="noopener noreferrer"
84+
>
85+
Go to Seer
86+
</SeerLink>
87+
</p>
88+
<MarketName>{marketName}</MarketName>
89+
<OutcomesHeading>Outcomes</OutcomesHeading>
90+
{filteredOutcomes?.map((outcome, index) => (
91+
<OutcomeItem key={index}>
92+
<OutcomeName>{outcome}</OutcomeName>
93+
</OutcomeItem>
94+
))}
95+
</Container>
96+
)
97+
}
98+
99+
export default SeerCardContent
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import React, { useState, useEffect } from 'react'
2+
import styled, { css } from 'styled-components'
3+
import { smallScreenStyle } from 'styles/small-screen-style'
4+
5+
interface ISeerExtraDetails {
6+
chainId: string
7+
contractAddress: string
8+
imagesIpfsHash: string
9+
}
10+
11+
interface MarketDetails {
12+
marketName: string
13+
marketImage: string
14+
outcomes: { name: string; image: string }[]
15+
}
16+
17+
const Container = styled.div`
18+
font-family: 'Arial';
19+
max-width: 600px;
20+
margin: 16px auto;
21+
padding: 20px;
22+
border: 1px solid #e0e0e0;
23+
border-radius: 8px;
24+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
25+
`
26+
27+
const LinkParagraph = styled.p`
28+
margin-bottom: 16px;
29+
`
30+
31+
const SeerLink = styled.a`
32+
color: #007bff;
33+
text-decoration: none;
34+
font-weight: bold;
35+
font-size: 16px;
36+
37+
&:hover,
38+
&:focus {
39+
text-decoration: underline;
40+
}
41+
`
42+
43+
const MarketHeader = styled.div`
44+
display: flex;
45+
flex-direction: row;
46+
gap: 16px;
47+
margin-bottom: 12px;
48+
49+
${smallScreenStyle(
50+
() => css`
51+
flex-wrap: wrap;
52+
`
53+
)}
54+
`
55+
56+
const MarketImage = styled.img`
57+
width: 64px;
58+
height: 64px;
59+
border-radius: 4px;
60+
`
61+
62+
const MarketName = styled.h3`
63+
margin: 0 0 12px;
64+
font-size: 1.5em;
65+
color: #333;
66+
`
67+
68+
const OutcomesHeading = styled.h4`
69+
margin: 0 0 12px;
70+
font-size: 1.2em;
71+
color: #555;
72+
`
73+
74+
const OutcomeItem = styled.div`
75+
display: flex;
76+
align-items: center;
77+
margin-bottom: 12px;
78+
padding: 8px;
79+
background-color: #f9f9f9;
80+
border-radius: 4px;
81+
`
82+
83+
const OutcomeImage = styled.img`
84+
max-width: 40px;
85+
height: auto;
86+
margin-right: 12px;
87+
border-radius: 4px;
88+
`
89+
90+
const OutcomeName = styled.span`
91+
font-size: 1em;
92+
color: #333;
93+
`
94+
95+
const ErrorMessage = styled.p`
96+
color: red;
97+
font-size: 14px;
98+
`
99+
100+
const LoadingMessage = styled.p`
101+
color: #666;
102+
font-size: 14px;
103+
`
104+
105+
const SeerExtraDetails: React.FC<ISeerExtraDetails> = ({
106+
chainId,
107+
contractAddress,
108+
imagesIpfsHash
109+
}) => {
110+
const [marketDetails, setMarketDetails] = useState<MarketDetails | null>(null)
111+
const [error, setError] = useState<string | null>(null)
112+
113+
useEffect(() => {
114+
const fetchData = async () => {
115+
try {
116+
let subgraphUrl
117+
if (chainId === '1')
118+
subgraphUrl = process.env.REACT_APP_SEER_SUBGRAPH_MAINNET ?? ''
119+
else if (chainId === '100')
120+
subgraphUrl = process.env.REACT_APP_SEER_SUBGRAPH_GNOSIS ?? ''
121+
else throw new Error(`Unsupported chainId: ${chainId}`)
122+
123+
const query = `
124+
{
125+
market(id: "${contractAddress.toLowerCase()}") {
126+
marketName
127+
outcomes
128+
}
129+
}
130+
`
131+
132+
const response = await fetch(subgraphUrl, {
133+
method: 'POST',
134+
headers: {
135+
'Content-Type': 'application/json'
136+
},
137+
body: JSON.stringify({ query })
138+
})
139+
140+
if (!response.ok) throw new Error('Subgraph query failed')
141+
142+
const data = await response.json()
143+
144+
if (!data.data.market) throw new Error('Market not found in subgraph')
145+
146+
const { marketName, outcomes } = data.data.market
147+
148+
const filteredOutcomes = outcomes.filter(
149+
(outcome: string) => outcome !== 'Invalid result'
150+
)
151+
152+
const ipfsResponse = await fetch(
153+
`${process.env.REACT_APP_IPFS_GATEWAY}${imagesIpfsHash}`
154+
)
155+
if (!ipfsResponse.ok) throw new Error('Failed to fetch IPFS data')
156+
const ipfsData = await ipfsResponse.json()
157+
const marketImage = ipfsData.market
158+
const outcomeImages = ipfsData.outcomes
159+
160+
const outcomesWithImages = filteredOutcomes.map(
161+
(name: string, index: number) => ({
162+
name,
163+
image: outcomeImages[index] || ''
164+
})
165+
)
166+
167+
setMarketDetails({
168+
marketName,
169+
marketImage,
170+
outcomes: outcomesWithImages
171+
})
172+
} catch (err) {
173+
setError(`Failed to load market details: ${err.message}`)
174+
console.error(err)
175+
}
176+
}
177+
178+
fetchData()
179+
}, [chainId, contractAddress, imagesIpfsHash])
180+
181+
if (error) return <ErrorMessage>{error}</ErrorMessage>
182+
183+
if (!marketDetails)
184+
return <LoadingMessage>Loading Seer details...</LoadingMessage>
185+
186+
const { marketName, marketImage, outcomes } = marketDetails
187+
188+
return (
189+
<Container>
190+
<LinkParagraph>
191+
<SeerLink
192+
href={`https://app.seer.pm/markets/${chainId}/${contractAddress}`}
193+
target="_blank"
194+
rel="noopener noreferrer"
195+
>
196+
Go to Seer
197+
</SeerLink>
198+
</LinkParagraph>
199+
<MarketHeader>
200+
<MarketImage
201+
src={`${process.env.REACT_APP_IPFS_GATEWAY}${marketImage}`}
202+
alt="Market"
203+
/>
204+
<MarketName>{marketName}</MarketName>
205+
</MarketHeader>
206+
<OutcomesHeading>Outcomes</OutcomesHeading>
207+
{outcomes.map((outcome, index) => (
208+
<OutcomeItem key={index}>
209+
<OutcomeImage
210+
src={`${process.env.REACT_APP_IPFS_GATEWAY}${outcome.image}`}
211+
alt={`Outcome ${index}`}
212+
/>
213+
<OutcomeName>{outcome.name}</OutcomeName>
214+
</OutcomeItem>
215+
))}
216+
</Container>
217+
)
218+
}
219+
220+
export default SeerExtraDetails

0 commit comments

Comments
 (0)