Skip to content

Commit 56b890f

Browse files
committed
Merge branch 'feature/allow-multiple-tokenIds'
2 parents 8828298 + 7616bcb commit 56b890f

File tree

3 files changed

+218
-33
lines changed

3 files changed

+218
-33
lines changed

src/components/CryptoKitties/CryptoKitties.tsx

Lines changed: 182 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ export interface FormDetails {
1313
transferSuccess: boolean,
1414
}
1515

16+
interface KittyStatus {
17+
id: string;
18+
transferrable: boolean;
19+
forSale: boolean;
20+
forSire: boolean;
21+
loading: boolean;
22+
error?: string;
23+
transferSuccess?: boolean;
24+
auctionCancelled?: boolean;
25+
}
26+
1627
const CryptoKitties: React.FC<{
1728
walletAddress: string,
1829
dapperWalletAddress: string,
@@ -32,6 +43,8 @@ const CryptoKitties: React.FC<{
3243
transferSuccess: false,
3344
}
3445

46+
const [kittyStatuses, setKittyStatuses] = useState<KittyStatus[]>([])
47+
3548
const [total, setTotal] = useState<number>(0)
3649
const [balance, setBalance] = useState<number>(0)
3750
const [formDetails, setFormDetails] = useState<FormDetails>(initFormState)
@@ -108,16 +121,28 @@ const CryptoKitties: React.FC<{
108121
}
109122
}
110123

111-
const handleTransfer = async () => {
124+
const handleTransfer = async (kittyId: string) => {
112125
setFormDetails(prevState => ({ ...prevState, loading: true }))
113126
const address = Contracts['Core'].addr
114-
const methodCall = core.methods.transfer(walletAddress, formDetails.kittyId)
127+
const methodCall = core.methods.transfer(walletAddress, kittyId)
115128
try {
116129
await invokeTx(address, methodCall, '0')
117-
setFormDetails(prevState => ({ ...prevState, transferrable: false, transferSuccess: true }))
130+
if (kittyStatuses.length > 0) {
131+
setKittyStatuses(prev => prev.map(s =>
132+
s.id === kittyId ? { ...s, transferSuccess: true } : s
133+
))
134+
} else {
135+
setFormDetails(prevState => ({ ...prevState, transferrable: false, transferSuccess: true }))
136+
}
118137
} catch (e) {
119138
console.log(e)
120-
alert('Failed to transfer. Please try again.')
139+
if (kittyStatuses.length > 0) {
140+
setKittyStatuses(prev => prev.map(s =>
141+
s.id === kittyId ? { ...s, error: 'Failed to transfer. Please try again.' } : s
142+
))
143+
} else {
144+
alert('Failed to transfer. Please try again.')
145+
}
121146
} finally {
122147
setFormDetails(prevState => ({ ...prevState, loading: false }))
123148
}
@@ -128,10 +153,75 @@ const CryptoKitties: React.FC<{
128153
const newState = { ...formDetails }
129154
if (changeParam === 'kittyId') {
130155
newState.kittyId = value
156+
157+
// Clear existing statuses when input changes
158+
setKittyStatuses([])
159+
160+
// If input contains commas, treat as multiple IDs
161+
const ids = value.split(',').map(id => id.trim()).filter(id => id !== '');
162+
if (ids.length > 0) {
163+
// Validate all IDs
164+
const validIds = ids.filter(id => /^\d+$/.test(id) && total && parseInt(id, 10) <= total);
165+
if (validIds.length !== ids.length) {
166+
alert('Some kitty IDs were invalid and will be ignored');
167+
}
168+
169+
// Initialize statuses for valid IDs
170+
setKittyStatuses(validIds.map(id => ({
171+
id,
172+
transferrable: false,
173+
forSale: false,
174+
forSire: false,
175+
loading: false
176+
})));
177+
}
131178
}
132179
setFormDetails(newState)
133180
}
134181

182+
const checkAllKitties = async () => {
183+
const ids = formDetails.kittyId.split(',').map(id => id.trim()).filter(id => /^\d+$/.test(id));
184+
for (let i = 0; i < ids.length; i++) {
185+
const kittyId = ids[i];
186+
setKittyStatuses(prev => prev.map(status =>
187+
status.id === kittyId ? { ...status, loading: true } : status
188+
));
189+
190+
try {
191+
const owner = await core.methods.ownerOf(kittyId).call();
192+
if (owner && owner.toString().toLowerCase() === dapperWalletAddress.toLowerCase()) {
193+
setKittyStatuses(prev => prev.map(status =>
194+
status.id === kittyId ? { ...status, transferrable: true, loading: false } : status
195+
));
196+
continue;
197+
}
198+
199+
const isInSaleAuction = await checkAuction(sale, kittyId);
200+
if (isInSaleAuction) {
201+
setKittyStatuses(prev => prev.map(status =>
202+
status.id === kittyId ? { ...status, forSale: true, loading: false } : status
203+
));
204+
continue;
205+
}
206+
207+
const isInSireAuction = await checkAuction(sire, kittyId);
208+
if (isInSireAuction) {
209+
setKittyStatuses(prev => prev.map(status =>
210+
status.id === kittyId ? { ...status, forSire: true, loading: false } : status
211+
));
212+
} else {
213+
setKittyStatuses(prev => prev.map(status =>
214+
status.id === kittyId ? { ...status, loading: false, error: 'Not owned by this Dapper Wallet' } : status
215+
));
216+
}
217+
} catch (error) {
218+
setKittyStatuses(prev => prev.map(status =>
219+
status.id === kittyId ? { ...status, loading: false, error: 'Error checking ownership' } : status
220+
));
221+
}
222+
}
223+
}
224+
135225
const formatBalance = (balance: number) => balance === 1 ? '1 CryptoKitty' : `${balance} CryptoKitties`
136226

137227
const resetForm = () => setFormDetails(initFormState)
@@ -144,34 +234,98 @@ const CryptoKitties: React.FC<{
144234
<p>{`Enter a CryptoKitty id from your Dapper Wallet to check if the kitty can be transferred.`}</p>
145235
<p>{`If the kitty is currently for sale or sire you will be prompted to cancel the auction.`}</p>
146236
<p>{`If you cancel the auction (assuming you created it) you will then be able to transfer the kitty.`}</p>
147-
<h3>{(formDetails.forSale || formDetails.forSire) ? `Cancel Auction:` : `Transfer Kitty:`}</h3>
148-
{formDetails.auctionCancelled || formDetails.transferSuccess ? (
149-
<>
150-
{formDetails.auctionCancelled ? (
151-
<p><span className={'success'}></span>{`Cancel auction method invoked for Kitty ID: #${formDetails.kittyId}`}</p>
237+
<div>
238+
<p><b>Enter a CryptoKitty ID or multiple IDs separated by commas:</b></p>
239+
<div>
240+
<input
241+
type={'text'}
242+
value={formDetails.kittyId}
243+
onChange={e => handleChange(e, 'kittyId')}
244+
disabled={formDetails.loading}
245+
className={'tokenId'}
246+
placeholder="Example: 123 or 123,456,789"
247+
/>
248+
{kittyStatuses.length > 0 ? (
249+
<button onClick={checkAllKitties} disabled={formDetails.loading}>
250+
Check Kitties
251+
</button>
152252
) : (
153-
<p><span className={'success'}></span>{`Transfer method invoked for Kitty ID: #${formDetails.kittyId}`}</p>
154-
)}
155-
<button onClick={resetForm}>{`Reset form`}</button>
156-
</>
157-
) : (
158-
<label>
159-
kitty id:
160-
<input type={'text'} value={formDetails.kittyId} onChange={e => handleChange(e, 'kittyId')} disabled={formDetails.loading} className={'tokenId'} />
161-
{formDetails.transferrable && (
162-
<button onClick={handleTransfer} disabled={formDetails.loading}>{`transfer kitty #${formDetails.kittyId}`}</button>
253+
<button onClick={handleCheckOwnership} disabled={formDetails.loading}>
254+
Check Ownership
255+
</button>
163256
)}
164-
{(formDetails.forSale || formDetails.forSire) && (
165-
<button onClick={handleCancelAuction} disabled={formDetails.loading}>{`cancel ${formDetails.forSale ? 'sale' : 'sire'} auction`}</button>
166-
)}
167-
{!formDetails.transferrable && !formDetails.forSale && !formDetails.forSire && (
168-
<button onClick={handleCheckOwnership} disabled={formDetails.loading}>{`check ownership`}</button>
169-
)}
170-
</label>
171-
)}
257+
</div>
258+
259+
{kittyStatuses.length > 0 ? (
260+
<div style={{ marginTop: '20px' }}>
261+
{kittyStatuses.map((status) => (
262+
<div key={status.id} style={{ marginBottom: '10px', padding: '10px', border: '1px solid #ccc' }}>
263+
<h4>Kitty #{status.id}</h4>
264+
{status.loading ? (
265+
<p>Checking status...</p>
266+
) : status.error ? (
267+
<p className="error">{status.error}</p>
268+
) : status.transferSuccess ? (
269+
<p><span className={'success'}></span>{`Transfer method invoked for Kitty ID: #${status.id}`}</p>
270+
) : status.auctionCancelled ? (
271+
<p><span className={'success'}></span>{`Cancel auction method invoked for Kitty ID: #${status.id}`}</p>
272+
) : (
273+
<div>
274+
{status.transferrable && (
275+
<button
276+
onClick={() => handleTransfer(status.id)}
277+
disabled={formDetails.loading}
278+
>
279+
Transfer Kitty
280+
</button>
281+
)}
282+
{(status.forSale || status.forSire) && (
283+
<button
284+
onClick={() => {
285+
setFormDetails(prev => ({
286+
...prev,
287+
kittyId: status.id,
288+
forSale: status.forSale,
289+
forSire: status.forSire
290+
}));
291+
handleCancelAuction();
292+
setKittyStatuses(prev => prev.map(s =>
293+
s.id === status.id ? { ...s, auctionCancelled: true } : s
294+
));
295+
}}
296+
disabled={formDetails.loading}
297+
>
298+
Cancel {status.forSale ? 'Sale' : 'Sire'} Auction
299+
</button>
300+
)}
301+
</div>
302+
)}
303+
</div>
304+
))}
305+
</div>
306+
) : formDetails.auctionCancelled || formDetails.transferSuccess ? (
307+
<>
308+
{formDetails.auctionCancelled ? (
309+
<p><span className={'success'}></span>{`Cancel auction method invoked for Kitty ID: #${formDetails.kittyId}`}</p>
310+
) : (
311+
<p><span className={'success'}></span>{`Transfer method invoked for Kitty ID: #${formDetails.kittyId}`}</p>
312+
)}
313+
<button onClick={resetForm}>{`Reset form`}</button>
314+
</>
315+
) : (
316+
<div style={{ marginTop: '10px' }}>
317+
{formDetails.transferrable && (
318+
<button onClick={() => handleTransfer(formDetails.kittyId)} disabled={formDetails.loading}>{`transfer kitty #${formDetails.kittyId}`}</button>
319+
)}
320+
{(formDetails.forSale || formDetails.forSire) && (
321+
<button onClick={handleCancelAuction} disabled={formDetails.loading}>{`cancel ${formDetails.forSale ? 'sale' : 'sire'} auction`}</button>
322+
)}
323+
</div>
324+
)}
325+
</div>
172326
</>
173327

174328
)
175329
}
176330

177-
export default CryptoKitties
331+
export default CryptoKitties

src/style/index.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,27 @@ code {
5353
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
5454
monospace;
5555
}
56+
57+
.error {
58+
color: #dc3545;
59+
font-weight: 500;
60+
}
61+
62+
textarea {
63+
padding: 8px;
64+
margin: 8px 0;
65+
border: 1px solid #ccc;
66+
border-radius: 4px;
67+
font-family: inherit;
68+
font-size: 14px;
69+
}
70+
71+
textarea:disabled {
72+
background-color: #e9ecef;
73+
cursor: not-allowed;
74+
}
75+
76+
button:disabled {
77+
opacity: 0.65;
78+
cursor: not-allowed;
79+
}

src/style/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const Main = styled.main`
4848
margin: 0 auto;
4949
max-width: 1592px;
5050
margin-top: 40px;
51+
padding-bottom: ${gutters['xxl']};
5152
> h1 {
5253
margin: ${gutters['xxl']} 0 ${gutters['lg']};
5354
}
@@ -60,11 +61,11 @@ export const Main = styled.main`
6061
margin-bottom: ${gutters['xl']};
6162
}
6263
}
64+
h4 {
65+
margin-bottom: ${gutters['md']};
66+
}
6367
> p {
6468
max-width: 900px;
65-
> b {
66-
font-weight: bold;
67-
}
6869
+ ul {
6970
margin-top: ${gutters['lg']};
7071
}
@@ -101,6 +102,12 @@ export const Main = styled.main`
101102
> label {
102103
font-weight: bold;
103104
}
105+
p > b {
106+
font-weight: bold;
107+
}
108+
> div {
109+
margin-top: ${gutters['md']};
110+
}
104111
input[type='text'], input[type='number'], textarea {
105112
padding: ${gutters['md']} ${gutters['lg']};
106113
font-family: monospace;
@@ -111,8 +118,8 @@ export const Main = styled.main`
111118
input[type='text'], textarea {
112119
max-width: 620px;
113120
&.tokenId {
114-
max-width: 320px;
115-
margin-left: ${gutters['lg']};
121+
margin-top: ${gutters['md']};
122+
116123
}
117124
}
118125
input[type='submit'] {

0 commit comments

Comments
 (0)