mirror of https://github.com/certusone/oyster.git
feat: create auction
This commit is contained in:
parent
825aca40ef
commit
523f01a719
|
@ -1,21 +1,12 @@
|
||||||
import React, { useLayoutEffect, useState } from 'react';
|
import React, { useLayoutEffect, useState } from 'react';
|
||||||
import { Card, Avatar } from 'antd';
|
import { Card, Avatar, CardProps } from 'antd';
|
||||||
import { MetadataCategory } from '@oyster/common';
|
import { MetadataCategory } from '@oyster/common';
|
||||||
import { ArtContent } from './../ArtContent';
|
import { ArtContent } from './../ArtContent';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
const { Meta } = Card;
|
const { Meta } = Card;
|
||||||
|
|
||||||
export const ArtCard = ({
|
export interface ArtCardProps extends CardProps {
|
||||||
image,
|
|
||||||
category,
|
|
||||||
name,
|
|
||||||
symbol,
|
|
||||||
description,
|
|
||||||
artist,
|
|
||||||
preview,
|
|
||||||
small,
|
|
||||||
}: {
|
|
||||||
image?: string;
|
image?: string;
|
||||||
category?: MetadataCategory
|
category?: MetadataCategory
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -23,13 +14,17 @@ export const ArtCard = ({
|
||||||
description?: string;
|
description?: string;
|
||||||
artist?: string;
|
artist?: string;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
small?: boolean
|
small?: boolean;
|
||||||
}) => {
|
}
|
||||||
|
|
||||||
|
export const ArtCard = (props: ArtCardProps) => {
|
||||||
|
const { className, small, category, image, name, preview, artist, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
hoverable={true}
|
hoverable={true}
|
||||||
className={`art-card ${small ? 'small' : ''}`}
|
className={`art-card ${small ? 'small' : ''} ${className}`}
|
||||||
cover={<ArtContent category={category} content={image} />}
|
cover={<ArtContent category={category} content={image} preview={preview} />}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
<Meta
|
<Meta
|
||||||
title={`${name}`}
|
title={`${name}`}
|
||||||
|
|
|
@ -2,11 +2,27 @@ import React, { useMemo } from 'react';
|
||||||
import { Image } from 'antd';
|
import { Image } from 'antd';
|
||||||
import { MetadataCategory } from '@oyster/common'
|
import { MetadataCategory } from '@oyster/common'
|
||||||
|
|
||||||
export const ArtContent = ({ content, category, className }: { category?: MetadataCategory, content?: string, className?: string }) => {
|
export const ArtContent = ({
|
||||||
|
content,
|
||||||
|
category,
|
||||||
|
className,
|
||||||
|
preview
|
||||||
|
}: {
|
||||||
|
category?: MetadataCategory,
|
||||||
|
content?: string,
|
||||||
|
className?: string,
|
||||||
|
preview?: boolean,
|
||||||
|
}) => {
|
||||||
return category === 'video' ?
|
return category === 'video' ?
|
||||||
<video src={content} className={className} playsInline={true} autoPlay={true} controlsList="nodownload" loop={true} /> :
|
<video src={content}
|
||||||
|
className={className}
|
||||||
|
playsInline={true}
|
||||||
|
autoPlay={true}
|
||||||
|
controlsList="nodownload"
|
||||||
|
loop={true} /> :
|
||||||
<Image
|
<Image
|
||||||
src={content}
|
src={content}
|
||||||
|
preview={preview}
|
||||||
wrapperClassName={className}
|
wrapperClassName={className}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './useArt';
|
export * from './useArt';
|
||||||
export * from './useAuctions';
|
export * from './useAuctions';
|
||||||
|
export * from './useUserArts';
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TokenAccount, useUserAccounts } from '@oyster/common';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useMeta } from './../contexts';
|
||||||
|
|
||||||
|
export const useUserArts = () => {
|
||||||
|
const { metadata } = useMeta();
|
||||||
|
const { userAccounts } = useUserAccounts();
|
||||||
|
const accountByMint = userAccounts.reduce((prev, acc) => {
|
||||||
|
prev.set(acc.info.mint.toBase58(), acc);
|
||||||
|
return prev;
|
||||||
|
}, new Map<string, TokenAccount>());
|
||||||
|
|
||||||
|
const ownedMetadata = metadata.filter(m => accountByMint.has(m.info.mint.toBase58()));
|
||||||
|
|
||||||
|
return ownedMetadata;
|
||||||
|
}
|
|
@ -1,20 +1,12 @@
|
||||||
import { ParsedAccount, TokenAccount, useUserAccounts } from '@oyster/common';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ArtCard } from '../../components/ArtCard';
|
import { ArtCard } from '../../components/ArtCard';
|
||||||
import { useMeta } from '../../contexts';
|
|
||||||
import { Row, Col } from 'antd';
|
import { Row, Col } from 'antd';
|
||||||
import Masonry from 'react-masonry-css'
|
import Masonry from 'react-masonry-css'
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useUserArts } from '../../hooks';
|
||||||
|
|
||||||
export const ArtworksView = () => {
|
export const ArtworksView = () => {
|
||||||
const { metadata } = useMeta();
|
const ownedMetadata = useUserArts();
|
||||||
const { userAccounts } = useUserAccounts();
|
|
||||||
const accountByMint = userAccounts.reduce((prev, acc) => {
|
|
||||||
prev.set(acc.info.mint.toBase58(), acc);
|
|
||||||
return prev;
|
|
||||||
}, new Map<string, TokenAccount>());
|
|
||||||
|
|
||||||
const ownedMetadata = metadata.filter(m => accountByMint.has(m.info.mint.toBase58()));
|
|
||||||
const breakpointColumnsObj = {
|
const breakpointColumnsObj = {
|
||||||
default: 4,
|
default: 4,
|
||||||
1100: 3,
|
1100: 3,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Steps,
|
Steps,
|
||||||
Row,
|
Row,
|
||||||
|
@ -12,28 +12,29 @@ import {
|
||||||
Spin,
|
Spin,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
|
TimePicker,
|
||||||
|
DatePicker,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { ArtCard } from './../../components/ArtCard';
|
import { ArtCard } from './../../components/ArtCard';
|
||||||
import { UserSearch, UserValue } from './../../components/UserSearch';
|
import { UserSearch, UserValue } from './../../components/UserSearch';
|
||||||
import { Confetti } from './../../components/Confetti';
|
import { Confetti } from './../../components/Confetti';
|
||||||
import './../styles.less';
|
import './../styles.less';
|
||||||
import { mintNFT } from '../../models';
|
|
||||||
import {
|
import {
|
||||||
MAX_METADATA_LEN,
|
MAX_METADATA_LEN,
|
||||||
MAX_OWNER_LEN,
|
MAX_OWNER_LEN,
|
||||||
MAX_URI_LENGTH,
|
MAX_URI_LENGTH,
|
||||||
Metadata,
|
|
||||||
NameSymbolTuple,
|
|
||||||
useConnection,
|
useConnection,
|
||||||
useWallet,
|
useWallet,
|
||||||
IMetadataExtension,
|
|
||||||
MetadataCategory,
|
|
||||||
useConnectionConfig,
|
useConnectionConfig,
|
||||||
|
Metadata,
|
||||||
|
ParsedAccount,
|
||||||
} from '@oyster/common';
|
} from '@oyster/common';
|
||||||
import { getAssetCostToStore, LAMPORT_MULTIPLIER } from '../../utils/assets';
|
import { getAssetCostToStore, LAMPORT_MULTIPLIER } from '../../utils/assets';
|
||||||
import { Connection } from '@solana/web3.js';
|
import { Connection, ParsedAccountData, PublicKey } from '@solana/web3.js';
|
||||||
import { MintLayout } from '@solana/spl-token';
|
import { MintLayout } from '@solana/spl-token';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { useUserArts } from '../../hooks';
|
||||||
|
import Masonry from 'react-masonry-css';
|
||||||
|
|
||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
@ -46,6 +47,28 @@ export enum AuctionCategory {
|
||||||
Collection
|
Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuctionState {
|
||||||
|
// Min price required for the item to sell
|
||||||
|
reservationPrice: number;
|
||||||
|
|
||||||
|
// listed NFTs
|
||||||
|
items: ParsedAccount<Metadata>[];
|
||||||
|
|
||||||
|
// number of editions for this auction (only applicable to limited edition)
|
||||||
|
editions?: number;
|
||||||
|
|
||||||
|
// date time when auction should start UTC+0
|
||||||
|
startDate?: Date;
|
||||||
|
|
||||||
|
// suggested date time when auction should end UTC+0
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
// time interval between highest bid and end of the auction
|
||||||
|
gapTime?: Date
|
||||||
|
|
||||||
|
category: AuctionCategory;
|
||||||
|
}
|
||||||
|
|
||||||
export const AuctionCreateView = () => {
|
export const AuctionCreateView = () => {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const { env } = useConnectionConfig();
|
const { env } = useConnectionConfig();
|
||||||
|
@ -56,15 +79,10 @@ export const AuctionCreateView = () => {
|
||||||
const [step, setStep] = useState<number>(0);
|
const [step, setStep] = useState<number>(0);
|
||||||
const [saving, setSaving] = useState<boolean>(false);
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
const [progress, setProgress] = useState<number>(0);
|
const [progress, setProgress] = useState<number>(0);
|
||||||
const [attributes, setAttributes] = useState<IMetadataExtension>({
|
const [attributes, setAttributes] = useState<AuctionState>({
|
||||||
name: '',
|
reservationPrice: 0,
|
||||||
symbol: '',
|
items: [],
|
||||||
description: '',
|
category: AuctionCategory.Single,
|
||||||
externalUrl: '',
|
|
||||||
image: '',
|
|
||||||
royalty: 0,
|
|
||||||
files: [],
|
|
||||||
category: MetadataCategory.Image,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -76,19 +94,9 @@ export const AuctionCreateView = () => {
|
||||||
history.push(`/auction/create/${_step.toString()}`)
|
history.push(`/auction/create/${_step.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// store files
|
const createAuction = async () => {
|
||||||
const mint = async () => {
|
// TODO: ....
|
||||||
const metadata = {
|
};
|
||||||
...(attributes as any),
|
|
||||||
image: attributes.files && attributes.files?.[0] && attributes.files[0].name,
|
|
||||||
files: (attributes?.files || []).map(f => f.name),
|
|
||||||
}
|
|
||||||
setSaving(true)
|
|
||||||
const inte = setInterval(() => setProgress(prog => prog + 1), 600)
|
|
||||||
// Update progress inside mintNFT
|
|
||||||
await mintNFT(connection, wallet, env, (attributes?.files || []), metadata)
|
|
||||||
clearInterval(inte)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -109,7 +117,7 @@ export const AuctionCreateView = () => {
|
||||||
<Col {...(saving ? { xl: 24 } : { xl: 16 })}>
|
<Col {...(saving ? { xl: 24 } : { xl: 16 })}>
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
<CategoryStep
|
<CategoryStep
|
||||||
confirm={(category: MetadataCategory) => {
|
confirm={(category: AuctionCategory) => {
|
||||||
setAttributes({
|
setAttributes({
|
||||||
...attributes,
|
...attributes,
|
||||||
category,
|
category,
|
||||||
|
@ -127,34 +135,27 @@ export const AuctionCreateView = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<InfoStep
|
<TermsStep
|
||||||
attributes={attributes}
|
attributes={attributes}
|
||||||
setAttributes={setAttributes}
|
setAttributes={setAttributes}
|
||||||
confirm={() => gotoStep(3)}
|
confirm={() => gotoStep(3)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
<RoyaltiesStep
|
<ReviewStep
|
||||||
attributes={attributes}
|
|
||||||
confirm={() => gotoStep(4)}
|
|
||||||
setAttributes={setAttributes}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{step === 4 && (
|
|
||||||
<LaunchStep
|
|
||||||
attributes={attributes}
|
attributes={attributes}
|
||||||
confirm={() => gotoStep(5)}
|
confirm={() => gotoStep(5)}
|
||||||
connection={connection}
|
connection={connection}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 5 && (
|
{step === 4 && (
|
||||||
<WaitingStep
|
<WaitingStep
|
||||||
mint={mint}
|
createAuction={createAuction}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
confirm={() => gotoStep(6)}
|
confirm={() => gotoStep(6)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 6 && (
|
{step === 5 && (
|
||||||
<Congrats />
|
<Congrats />
|
||||||
)}
|
)}
|
||||||
{(0 < step && step < 5) && <Button onClick={() => gotoStep(step - 1)}>Back</Button>}
|
{(0 < step && step < 5) && <Button onClick={() => gotoStep(step - 1)}>Back</Button>}
|
||||||
|
@ -164,7 +165,7 @@ export const AuctionCreateView = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CategoryStep = (props: { confirm: (category: MetadataCategory) => void }) => {
|
const CategoryStep = (props: { confirm: (category: AuctionCategory) => void }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className="call-to-action">
|
<Row className="call-to-action">
|
||||||
|
@ -179,7 +180,7 @@ const CategoryStep = (props: { confirm: (category: MetadataCategory) => void })
|
||||||
<Button
|
<Button
|
||||||
className="type-btn"
|
className="type-btn"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => props.confirm(MetadataCategory.Image)}
|
onClick={() => props.confirm(AuctionCategory.Single)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div>Single Artwork</div>
|
<div>Single Artwork</div>
|
||||||
|
@ -191,7 +192,7 @@ const CategoryStep = (props: { confirm: (category: MetadataCategory) => void })
|
||||||
<Button
|
<Button
|
||||||
className="type-btn"
|
className="type-btn"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => props.confirm(MetadataCategory.Video)}
|
onClick={() => props.confirm(AuctionCategory.Limited)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div>Limited Edition</div>
|
<div>Limited Edition</div>
|
||||||
|
@ -203,7 +204,7 @@ const CategoryStep = (props: { confirm: (category: MetadataCategory) => void })
|
||||||
<Button
|
<Button
|
||||||
className="type-btn"
|
className="type-btn"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => props.confirm(MetadataCategory.Audio)}
|
onClick={() => props.confirm(AuctionCategory.Open)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div>Open Edition</div>
|
<div>Open Edition</div>
|
||||||
|
@ -216,7 +217,7 @@ const CategoryStep = (props: { confirm: (category: MetadataCategory) => void })
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="type-btn"
|
className="type-btn"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => props.confirm(MetadataCategory.Audio)}
|
onClick={() => props.confirm(AuctionCategory.Collection)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div>Collection (Coming Soon)</div>
|
<div>Collection (Coming Soon)</div>
|
||||||
|
@ -231,21 +232,27 @@ const CategoryStep = (props: { confirm: (category: MetadataCategory) => void })
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectItemsStep = (props: {
|
const SelectItemsStep = (props: {
|
||||||
attributes: IMetadataExtension;
|
attributes: AuctionState;
|
||||||
setAttributes: (attr: IMetadataExtension) => void;
|
setAttributes: (attr: AuctionState) => void;
|
||||||
confirm: () => void;
|
confirm: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [mainFile, setMainFile] = useState<any>()
|
const items = useUserArts();
|
||||||
const [coverFile, setCoverFile] = useState<any>()
|
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set(props.attributes.items.map(item => item.pubkey.toBase58())));
|
||||||
const [image, setImage] = useState<string>("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.setAttributes({
|
props.setAttributes({
|
||||||
...props.attributes,
|
...props.attributes,
|
||||||
files: []
|
// TODO: add items
|
||||||
|
items: items.filter(item => selectedItems.has(item.pubkey.toBase58()))
|
||||||
})
|
})
|
||||||
}, [])
|
}, [selectedItems]);
|
||||||
|
|
||||||
|
const breakpointColumnsObj = {
|
||||||
|
default: 4,
|
||||||
|
1100: 3,
|
||||||
|
700: 2,
|
||||||
|
500: 1
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -256,6 +263,37 @@ const SelectItemsStep = (props: {
|
||||||
</p>
|
</p>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="content-action">
|
<Row className="content-action">
|
||||||
|
<Masonry
|
||||||
|
breakpointCols={breakpointColumnsObj}
|
||||||
|
className="my-masonry-grid"
|
||||||
|
columnClassName="my-masonry-grid_column"
|
||||||
|
>
|
||||||
|
{items.map(m => {
|
||||||
|
const id = m.pubkey.toBase58();
|
||||||
|
const isSelected = selectedItems.has(id);
|
||||||
|
|
||||||
|
const onSelect = () => {
|
||||||
|
let list = [...selectedItems.keys()];
|
||||||
|
if (props.attributes.category !== AuctionCategory.Collection) {
|
||||||
|
list = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected ?
|
||||||
|
setSelectedItems(new Set(list.filter(item => item !== id))) :
|
||||||
|
setSelectedItems(new Set([...list, id]));
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ArtCard key={id}
|
||||||
|
image={m.info.extended?.image}
|
||||||
|
category={m.info.extended?.category}
|
||||||
|
name={m.info?.name}
|
||||||
|
symbol={m.info.symbol}
|
||||||
|
preview={false}
|
||||||
|
onClick={onSelect}
|
||||||
|
className={isSelected ? 'selected-card' : ''}
|
||||||
|
/>;
|
||||||
|
})}
|
||||||
|
</Masonry>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Button
|
<Button
|
||||||
|
@ -264,23 +302,22 @@ const SelectItemsStep = (props: {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.setAttributes({
|
props.setAttributes({
|
||||||
...props.attributes,
|
...props.attributes,
|
||||||
files: [mainFile, coverFile].filter(f => f),
|
|
||||||
image,
|
|
||||||
})
|
})
|
||||||
props.confirm()
|
props.confirm()
|
||||||
}}
|
}}
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
>
|
>
|
||||||
Continue to Mint
|
Continue to Terms
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InfoStep = (props: {
|
const TermsStep = (props: {
|
||||||
attributes: IMetadataExtension;
|
attributes: AuctionState;
|
||||||
setAttributes: (attr: IMetadataExtension) => void;
|
setAttributes: (attr: AuctionState) => void;
|
||||||
confirm: () => void;
|
confirm: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [creators, setCreators] = useState<Array<UserValue>>([]);
|
const [creators, setCreators] = useState<Array<UserValue>>([]);
|
||||||
|
@ -288,76 +325,61 @@ const InfoStep = (props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className="call-to-action">
|
<Row className="call-to-action">
|
||||||
<h2>Describe your creation</h2>
|
<h2>Specify the terms of your auction</h2>
|
||||||
<p>
|
<p>
|
||||||
Provide detailed description of your creative process to engage with
|
Provide detailed auction parameters such as price, start time, etc.
|
||||||
your audience.
|
|
||||||
</p>
|
</p>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="content-action">
|
<Row className="content-action">
|
||||||
<Col xl={12}>
|
<Col className="section" xl={24}>
|
||||||
{props.attributes.image && (
|
|
||||||
<ArtCard
|
|
||||||
image={props.attributes.image}
|
|
||||||
category={props.attributes.category}
|
|
||||||
name={props.attributes.name}
|
|
||||||
symbol={props.attributes.symbol}
|
|
||||||
small={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
<Col className="section" xl={12}>
|
|
||||||
<label className="action-field">
|
<label className="action-field">
|
||||||
<span className="field-title">Title</span>
|
<span className="field-title">Price Floor (USD)</span>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
className="input"
|
className="input"
|
||||||
placeholder="Max 50 characters"
|
placeholder="Enter reservation price"
|
||||||
allowClear
|
allowClear
|
||||||
value={props.attributes.name}
|
|
||||||
onChange={info =>
|
onChange={info =>
|
||||||
props.setAttributes({
|
props.setAttributes({
|
||||||
...props.attributes,
|
...props.attributes,
|
||||||
name: info.target.value,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<span className="field-info">= ◎ 4.84</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="action-field">
|
<label className="action-field">
|
||||||
<span className="field-title">Symbol</span>
|
<span className="field-title">Tick Size (USD)</span>
|
||||||
<Input
|
<Input
|
||||||
className="input"
|
className="input"
|
||||||
placeholder="Max 10 characters"
|
placeholder="Enter tick size"
|
||||||
allowClear
|
allowClear
|
||||||
value={props.attributes.symbol}
|
|
||||||
onChange={info =>
|
onChange={info =>
|
||||||
props.setAttributes({
|
props.setAttributes({
|
||||||
...props.attributes,
|
...props.attributes,
|
||||||
symbol: info.target.value,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<span className="field-info">= ◎ 4.84</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="action-field">
|
<label className="action-field">
|
||||||
<span className="field-title">Creators</span>
|
<span className="field-title">Preview Start Date</span>
|
||||||
<UserSearch
|
<DatePicker className="field-date" size="large" />
|
||||||
setCreators={setCreators}
|
<TimePicker className="field-date" size="large" />
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="action-field">
|
<label className="action-field">
|
||||||
<span className="field-title">Description</span>
|
<span className="field-title">When do you want the auction to begin?</span>
|
||||||
<Input.TextArea
|
<span>Immediately</span>
|
||||||
className="input textarea"
|
<span>At a specified data</span>
|
||||||
placeholder="Max 500 characters"
|
</label>
|
||||||
value={props.attributes.description}
|
<label className="action-field">
|
||||||
onChange={info =>
|
<span className="field-title">Auction Start Date</span>
|
||||||
props.setAttributes({
|
<DatePicker className="field-date" size="large" />
|
||||||
...props.attributes,
|
<TimePicker className="field-date" size="large" />
|
||||||
description: info.target.value,
|
</label>
|
||||||
})
|
<label className="action-field">
|
||||||
}
|
<span className="field-title">End Start Date</span>
|
||||||
allowClear
|
<DatePicker className="field-date" size="large" />
|
||||||
/>
|
<TimePicker className="field-date" size="large" />
|
||||||
</label>
|
</label>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -368,81 +390,18 @@ const InfoStep = (props: {
|
||||||
onClick={props.confirm}
|
onClick={props.confirm}
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
>
|
>
|
||||||
Continue to royalties
|
Continue to Review
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoyaltiesStep = (props: {
|
const ReviewStep = (props: {
|
||||||
attributes: IMetadataExtension;
|
|
||||||
setAttributes: (attr: IMetadataExtension) => void;
|
|
||||||
confirm: () => void;
|
confirm: () => void;
|
||||||
}) => {
|
attributes: AuctionState;
|
||||||
const file = props.attributes.image;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Row className="call-to-action">
|
|
||||||
<h2>Set royalties for the creation</h2>
|
|
||||||
<p>
|
|
||||||
A royalty is a payment made by the seller of this item to the creator.
|
|
||||||
It is charged after every successful auction.
|
|
||||||
</p>
|
|
||||||
</Row>
|
|
||||||
<Row className="content-action">
|
|
||||||
<Col xl={12}>
|
|
||||||
{file && (
|
|
||||||
<ArtCard
|
|
||||||
image={props.attributes.image}
|
|
||||||
category={props.attributes.category}
|
|
||||||
name={props.attributes.name}
|
|
||||||
symbol={props.attributes.symbol}
|
|
||||||
small={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
<Col className="section" xl={12}>
|
|
||||||
<label className="action-field">
|
|
||||||
<span className="field-title">Royalty Percentage</span>
|
|
||||||
<InputNumber
|
|
||||||
autoFocus
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
placeholder="Between 0 and 100"
|
|
||||||
onChange={(val: number) => {
|
|
||||||
props.setAttributes({ ...props.attributes, royalty: val });
|
|
||||||
}}
|
|
||||||
className="royalties-input"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={props.confirm}
|
|
||||||
className="action-btn"
|
|
||||||
>
|
|
||||||
Continue to review
|
|
||||||
</Button>
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const LaunchStep = (props: {
|
|
||||||
confirm: () => void;
|
|
||||||
attributes: IMetadataExtension;
|
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
}) => {
|
}) => {
|
||||||
const files = props.attributes.files || [];
|
|
||||||
const metadata = {
|
|
||||||
...(props.attributes as any),
|
|
||||||
files: files.map(f => f?.name),
|
|
||||||
};
|
|
||||||
const [cost, setCost] = useState(0);
|
const [cost, setCost] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const rentCall = Promise.all([
|
const rentCall = Promise.all([
|
||||||
|
@ -457,46 +416,27 @@ const LaunchStep = (props: {
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
getAssetCostToStore([
|
// TODO: add
|
||||||
...files,
|
}, [setCost]);
|
||||||
new File([JSON.stringify(metadata)], 'metadata.json'),
|
|
||||||
]).then(async lamports => {
|
|
||||||
const sol = lamports / LAMPORT_MULTIPLIER;
|
|
||||||
|
|
||||||
// TODO: cache this and batch in one call
|
let item = props.attributes.items?.[0];
|
||||||
const [mintRent, metadataRent, nameSymbolRent] = await rentCall;
|
|
||||||
|
|
||||||
const uriStr = 'x';
|
|
||||||
let uriBuilder = '';
|
|
||||||
for (let i = 0; i < MAX_URI_LENGTH; i++) {
|
|
||||||
uriBuilder += uriStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalSol =
|
|
||||||
(metadataRent + nameSymbolRent + mintRent) / LAMPORT_MULTIPLIER;
|
|
||||||
|
|
||||||
// TODO: add fees based on number of transactions and signers
|
|
||||||
setCost(sol + additionalSol);
|
|
||||||
});
|
|
||||||
}, [...files, setCost]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className="call-to-action">
|
<Row className="call-to-action">
|
||||||
<h2>Launch your creation</h2>
|
<h2>Review and list</h2>
|
||||||
<p>
|
<p>
|
||||||
Provide detailed description of your creative process to engage with
|
Review your listing before publishing.
|
||||||
your audience.
|
|
||||||
</p>
|
</p>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="content-action">
|
<Row className="content-action">
|
||||||
<Col xl={12}>
|
<Col xl={12}>
|
||||||
{props.attributes.image && (
|
{item?.info && (
|
||||||
<ArtCard
|
<ArtCard
|
||||||
image={props.attributes.image}
|
image={item.info.extended?.image}
|
||||||
category={props.attributes.category}
|
category={item.info.extended?.category}
|
||||||
name={props.attributes.name}
|
name={item.info.name}
|
||||||
symbol={props.attributes.symbol}
|
symbol={item.info.symbol}
|
||||||
small={true}
|
small={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -504,9 +444,8 @@ const LaunchStep = (props: {
|
||||||
<Col className="section" xl={12}>
|
<Col className="section" xl={12}>
|
||||||
<Statistic
|
<Statistic
|
||||||
className="create-statistic"
|
className="create-statistic"
|
||||||
title="Royalty Percentage"
|
title="Copies"
|
||||||
value={props.attributes.royalty}
|
value={props.attributes.editions === undefined ? 'Unique' : props.attributes.editions }
|
||||||
suffix="%"
|
|
||||||
/>
|
/>
|
||||||
{cost ? (
|
{cost ? (
|
||||||
<Statistic
|
<Statistic
|
||||||
|
@ -527,15 +466,7 @@ const LaunchStep = (props: {
|
||||||
onClick={props.confirm}
|
onClick={props.confirm}
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
>
|
>
|
||||||
Pay with SOL
|
Publish Auction
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={true}
|
|
||||||
size="large"
|
|
||||||
onClick={props.confirm}
|
|
||||||
className="action-btn"
|
|
||||||
>
|
|
||||||
Pay with Credit Card
|
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
|
@ -543,14 +474,14 @@ const LaunchStep = (props: {
|
||||||
};
|
};
|
||||||
|
|
||||||
const WaitingStep = (props: {
|
const WaitingStep = (props: {
|
||||||
mint: Function,
|
createAuction: () => Promise<void>,
|
||||||
progress: number,
|
progress: number,
|
||||||
confirm: Function,
|
confirm: () => void,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const func = async () => {
|
const func = async () => {
|
||||||
await props.mint()
|
await props.createAuction()
|
||||||
props.confirm()
|
props.confirm()
|
||||||
}
|
}
|
||||||
func()
|
func()
|
||||||
|
@ -563,9 +494,9 @@ const WaitingStep = (props: {
|
||||||
percent={props.progress}
|
percent={props.progress}
|
||||||
/>
|
/>
|
||||||
<div className="waiting-title">
|
<div className="waiting-title">
|
||||||
Your creation is being uploaded to the decentralized web...
|
Your creation is being listed with Metaplex...
|
||||||
</div>
|
</div>
|
||||||
<div className="waiting-subtitle">This can take up to 1 minute.</div>
|
<div className="waiting-subtitle">This can take up to 30 seconds.</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -574,7 +505,7 @@ const Congrats = () => {
|
||||||
return <>
|
return <>
|
||||||
<div style={{ marginTop: 70 }}>
|
<div style={{ marginTop: 70 }}>
|
||||||
<div className="waiting-title">
|
<div className="waiting-title">
|
||||||
Congratulations! Your creation is now live.
|
Congratulations! Your auction is now live.
|
||||||
</div>
|
</div>
|
||||||
<div className="congrats-button-container">
|
<div className="congrats-button-container">
|
||||||
<Button className="congrats-button"><span>Share it on Twitter</span><span>></span></Button>
|
<Button className="congrats-button"><span>Share it on Twitter</span><span>></span></Button>
|
||||||
|
|
|
@ -166,6 +166,7 @@
|
||||||
.action-field {
|
.action-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
.field-title {
|
.field-title {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -185,9 +186,29 @@
|
||||||
margin: 12px 0px;
|
margin: 12px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-info {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-date {
|
||||||
|
background: #282828;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
border-width: 0px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-card {
|
||||||
|
border-width: 3px;
|
||||||
|
border-color: #5870EE !important;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
.royalties-input {
|
.royalties-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
|
Loading…
Reference in New Issue