-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1302 from matiasbenary/feat/add-nft
Mint nft tool added
- Loading branch information
Showing
6 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { Button, FileInput, Flex, Form, Input, openToast, Text } from '@near-pagoda/ui'; | ||
import { useContext } from 'react'; | ||
import type { SubmitHandler } from 'react-hook-form'; | ||
import { Controller, useForm } from 'react-hook-form'; | ||
|
||
import { NearContext } from '../WalletSelector'; | ||
|
||
type FormData = { | ||
title: string; | ||
description: string; | ||
image: FileList; | ||
}; | ||
|
||
interface IPFSResponse { | ||
cid: string; | ||
} | ||
|
||
const MAX_FILE_SIZE = 5 * 1024 * 1024; | ||
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']; | ||
|
||
const MintNft = () => { | ||
const { | ||
control, | ||
register, | ||
handleSubmit, | ||
formState: { errors, isSubmitting }, | ||
} = useForm<FormData>(); | ||
|
||
const { wallet, signedAccountId } = useContext(NearContext); | ||
|
||
const validateImage = (files: FileList) => { | ||
if (files.length === 0) return 'Image is required'; | ||
const file = files[0]; | ||
if (file.size > MAX_FILE_SIZE) return 'Image size should be less than 5MB'; | ||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) return 'Not a valid image format'; | ||
return true; | ||
}; | ||
|
||
const onSubmit: SubmitHandler<FormData> = async (data) => { | ||
if (!wallet) throw new Error('Wallet has not initialized yet'); | ||
try { | ||
let file = ''; | ||
|
||
if (data.image[0]) { | ||
const res = await fetch('https://ipfs.near.social/add', { | ||
method: 'POST', | ||
headers: { Accept: 'application/json' }, | ||
body: data.image[0], | ||
}); | ||
const fileData: IPFSResponse = await res.json(); | ||
file = fileData.cid; | ||
} | ||
|
||
const args = { | ||
receiver_id: signedAccountId, | ||
token_id: crypto.randomUUID(), | ||
token_metadata: { | ||
media: `https://ipfs.near.social/ipfs/${file}`, | ||
title: data.title, | ||
description: data.description, | ||
}, | ||
}; | ||
|
||
const string_args = JSON.stringify(args); | ||
|
||
// TODO: Improve, we estimate the cost as 3 times the cost of storing the args | ||
const cost_per_byte = 10 ** 19; | ||
const estimated_cost = string_args.length * cost_per_byte * 3; | ||
|
||
await wallet.signAndSendTransactions({ | ||
transactions: [ | ||
{ | ||
receiverId: 'nft.primitives.near', | ||
actions: [ | ||
{ | ||
type: 'FunctionCall', | ||
params: { | ||
methodName: 'nft_mint', | ||
args, | ||
gas: '300000000000000', | ||
deposit: estimated_cost, | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}); | ||
|
||
openToast({ | ||
type: 'success', | ||
title: 'Form Submitted', | ||
description: 'Your form has been submitted successfully', | ||
duration: 5000, | ||
}); | ||
} catch (error) { | ||
openToast({ | ||
type: 'error', | ||
title: 'Error', | ||
description: 'Failed to submit form', | ||
duration: 5000, | ||
}); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<Text size="text-l" style={{ marginBottom: '12px' }}> | ||
{' '} | ||
Mint NFT{' '} | ||
</Text> | ||
<Form onSubmit={handleSubmit(onSubmit)}> | ||
<Flex stack gap="l"> | ||
<Input | ||
label="Title" | ||
placeholder="Enter title" | ||
error={errors.title?.message} | ||
{...register('title', { required: 'Title is required' })} | ||
/> | ||
<Input | ||
label="Description" | ||
placeholder="Enter description" | ||
error={errors.description?.message} | ||
{...register('description', { required: 'Description is required' })} | ||
/> | ||
<div style={{ display: 'flex', flexDirection: 'column' }}> | ||
<Controller | ||
control={control} | ||
name="image" | ||
rules={{ | ||
required: 'Image is required', | ||
validate: validateImage, | ||
}} | ||
render={({ field, fieldState }) => ( | ||
<FileInput | ||
label="Image Upload" | ||
accept={ACCEPTED_IMAGE_TYPES.join(',')} | ||
error={fieldState.error?.message} | ||
{...field} | ||
value={field.value ? Array.from(field.value) : []} | ||
onChange={(value: File[] | null) => { | ||
const files = value; | ||
field.onChange(files); | ||
}} | ||
/> | ||
)} | ||
/> | ||
<span style={{ fontSize: '0.8rem', color: 'gray' }}> | ||
Accepted Formats: PNG, JPEG, GIF, SVG | Ideal dimension: 1:1 | Max size: 5MB | ||
</span> | ||
</div> | ||
<Button label="Mint me" variant="affirmative" type="submit" loading={isSubmitting} /> | ||
</Flex> | ||
</Form> | ||
</> | ||
); | ||
}; | ||
|
||
export default MintNft; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { Text } from '@near-pagoda/ui'; | ||
import Image from 'next/image'; | ||
import styled from 'styled-components'; | ||
|
||
import MintBase from '@/assets/images/mintbase.svg'; | ||
import Paras from '@/assets/images/paras.svg'; | ||
|
||
import MintNft from './MintNft'; | ||
|
||
const StyledButton = styled.a` | ||
background-color: #1e2030; | ||
color: #fff; | ||
border: none; | ||
border-radius: 25px; | ||
padding: 10px 20px; | ||
font-size: 16px; | ||
cursor: pointer; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
transition: background-color 0.3s; | ||
&:hover { | ||
background-color: #101124; | ||
} | ||
`; | ||
|
||
const MintbaseButton = styled(StyledButton)` | ||
background-color: #1e2030; | ||
&:hover { | ||
background-color: #282b3b; | ||
} | ||
`; | ||
|
||
const ParasButton = styled(StyledButton)` | ||
background-color: #050330; | ||
&:hover { | ||
background-color: #101438; | ||
} | ||
`; | ||
|
||
const NonFungibleToken = () => { | ||
return ( | ||
<> | ||
<MintNft /> | ||
<Text size="text-l" style={{ margin: '12px 0 0 0' }}> | ||
{' '} | ||
Community tools{' '} | ||
</Text> | ||
<Text>For more advanced options use community tools:</Text> | ||
<MintbaseButton href="https://paras.id/" target="_blank"> | ||
<Image alt="Mintbase Logo" src={MintBase} width={85} />{' '} | ||
</MintbaseButton> | ||
<ParasButton href="https://paras.id/" target="_blank"> | ||
<Image alt="Paras Logo" src={Paras} width={85} />{' '} | ||
</ParasButton> | ||
</> | ||
); | ||
}; | ||
|
||
export default NonFungibleToken; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Button, Card, Container, Flex, Section, SvgIcon, Tabs, Text } from '@near-pagoda/ui'; | ||
import { Coin, Gift, ImagesSquare } from '@phosphor-icons/react'; | ||
import { useRouter } from 'next/router'; | ||
import { useContext } from 'react'; | ||
|
||
import NonFungibleToken from '@/components/tools/NonFungibleToken'; | ||
import { NearContext } from '@/components/WalletSelector'; | ||
import { useDefaultLayout } from '@/hooks/useLayout'; | ||
import { useSignInRedirect } from '@/hooks/useSignInRedirect'; | ||
import type { NextPageWithLayout } from '@/utils/types'; | ||
|
||
const ToolsPage: NextPageWithLayout = () => { | ||
const router = useRouter(); | ||
const selectedTab = (router.query.tab as string) || 'ft'; | ||
const { signedAccountId } = useContext(NearContext); | ||
|
||
const { requestAuthentication } = useSignInRedirect(); | ||
return ( | ||
<Section grow="available" style={{ background: 'var(--sand3)' }}> | ||
<Container size="s"> | ||
<Flex stack gap="l"> | ||
<Text as="h1" size="text-2xl"> | ||
Tools | ||
</Text> | ||
|
||
{signedAccountId ? ( | ||
<Card style={{ paddingTop: 0 }}> | ||
<Tabs.Root value={selectedTab}> | ||
<Tabs.List style={{ marginBottom: 'var(--gap-m)' }}> | ||
<Tabs.Trigger href="?tab=ft" value="ft"> | ||
<SvgIcon icon={<Coin fill="bold" />} /> | ||
FT | ||
</Tabs.Trigger> | ||
|
||
<Tabs.Trigger href="?tab=nft" value="nft"> | ||
<SvgIcon icon={<ImagesSquare fill="bold" />} /> | ||
NFT | ||
</Tabs.Trigger> | ||
|
||
<Tabs.Trigger href="?tab=linkdrops" value="linkdrops"> | ||
<SvgIcon icon={<Gift fill="bold" />} /> | ||
Linkdrops | ||
</Tabs.Trigger> | ||
</Tabs.List> | ||
|
||
<Tabs.Content value="ft"> | ||
<Text>Coming soon</Text> | ||
</Tabs.Content> | ||
|
||
<Tabs.Content value="nft"> | ||
<NonFungibleToken /> | ||
</Tabs.Content> | ||
|
||
<Tabs.Content value="linkdrops"> | ||
<Text>Coming soon</Text> | ||
</Tabs.Content> | ||
</Tabs.Root> | ||
</Card> | ||
) : ( | ||
<Card> | ||
<Text>Please sign in to use wallet utilities.</Text> | ||
<Button label="Sign In" fill="outline" onClick={() => requestAuthentication()} /> | ||
</Card> | ||
)} | ||
</Flex> | ||
</Container> | ||
</Section> | ||
); | ||
}; | ||
|
||
ToolsPage.getLayout = useDefaultLayout; | ||
|
||
export default ToolsPage; |