diff --git a/packages/metavinci/src/App.less b/packages/metavinci/src/App.less
index 1bdb2fb..f0d62c2 100644
--- a/packages/metavinci/src/App.less
+++ b/packages/metavinci/src/App.less
@@ -57,6 +57,11 @@ code {
}
+.ant-upload.ant-upload-drag {
+ border-radius: 8px;
+ border-color: transparent;
+}
+
.tab-title {
font-family: Inter;
font-style: normal;
diff --git a/packages/metavinci/src/components/Confetti/index.tsx b/packages/metavinci/src/components/Confetti/index.tsx
new file mode 100644
index 0000000..3c9ae56
--- /dev/null
+++ b/packages/metavinci/src/components/Confetti/index.tsx
@@ -0,0 +1,53 @@
+import React, { useEffect, useState } from "react";
+
+interface Particle {
+ top: number,
+ left: number,
+ speed: number,
+ angle: number,
+ angle_speed: number,
+ size: number,
+}
+
+export const Confetti = () => {
+ const [particles, setParticles] = useState>(Array.from({ length: 30 }).map(_ => ({
+ top: -Math.random() * 100,
+ left: Math.random() * 100,
+ speed: Math.random() * 1 + 3,
+ angle: 0,
+ angle_speed: Math.random() * 10 + 5,
+ size: Math.floor(Math.random() * 8 + 12),
+ })))
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setParticles(parts => parts.map(part => ({
+ ...part,
+ top: (part.top > 130 ? -30 : part.top + part.speed),
+ angle: (part.angle > 360 ? 0 : part.angle + part.angle_speed),
+ })))
+ }, 70);
+
+ const timeout = setTimeout(() => {
+ setParticles([]);
+ clearInterval(interval);
+ }, 5000);
+
+ return () => {
+ clearInterval(interval);
+ clearTimeout(timeout);
+ };
+ }, [])
+
+ const getStyle = (particle: Particle) => ({
+ top: `${particle.top}%`,
+ left: `${particle.left}%`,
+ transform: `rotate(${particle.angle}deg)`,
+ width: particle.size,
+ height: particle.size,
+ })
+
+ return
+ {particles.map((particle, idx) => )}
+
+}
diff --git a/packages/metavinci/src/routes.tsx b/packages/metavinci/src/routes.tsx
index 55f90e1..20c92f9 100644
--- a/packages/metavinci/src/routes.tsx
+++ b/packages/metavinci/src/routes.tsx
@@ -57,13 +57,13 @@ export function Routes() {
/>
}
+ path="/auction/create/:step_param?"
+ component={() => }
/>
}
+ path="/auction/:id"
+ component={() => }
/>
diff --git a/packages/metavinci/src/views/artCreate/index.tsx b/packages/metavinci/src/views/artCreate/index.tsx
index 8a2512b..d456dba 100644
--- a/packages/metavinci/src/views/artCreate/index.tsx
+++ b/packages/metavinci/src/views/artCreate/index.tsx
@@ -15,7 +15,8 @@ import {
} from 'antd';
import { ArtCard } from './../../components/ArtCard';
import { UserSearch } from './../../components/UserSearch';
-import './styles.less';
+import { Confetti } from './../../components/Confetti';
+import './../styles.less';
import { mintNFT } from '../../models';
import {
MAX_METADATA_LEN,
@@ -167,27 +168,58 @@ const CategoryStep = (props: { confirm: (category: MetadataCategory) => void })
- props.confirm(MetadataCategory.Image)}
- >
- Image
-
- props.confirm(MetadataCategory.Video)}
- >
- Video
-
- props.confirm(MetadataCategory.Audio)}
- >
- Audio
-
+
+
+ props.confirm(MetadataCategory.Image)}
+ >
+
+
Image
+
JPG, PNG, GIF
+
+
+
+
+ props.confirm(MetadataCategory.Video)}
+ >
+
+
Video
+
MP3, WAV, FLAC
+
+
+
+
+
+ props.confirm(MetadataCategory.Audio)}
+ >
+
+
+
+
+ props.confirm(MetadataCategory.Audio)}
+ >
+
+
+
+
>
);
@@ -623,54 +655,3 @@ const Congrats = () => {
>
}
-interface Particle {
- top: number,
- left: number,
- speed: number,
- angle: number,
- angle_speed: number,
- size: number,
-}
-
-const Confetti = () => {
- const [particles, setParticles] = useState>(Array.from({ length: 30 }).map(_ => ({
- top: -Math.random() * 100,
- left: Math.random() * 100,
- speed: Math.random() * 1 + 3,
- angle: 0,
- angle_speed: Math.random() * 10 + 5,
- size: Math.floor(Math.random() * 8 + 12),
- })))
-
- useEffect(() => {
- const interval = setInterval(() => {
- setParticles(parts => parts.map(part => ({
- ...part,
- top: (part.top > 130 ? -30 : part.top + part.speed),
- angle: (part.angle > 360 ? 0 : part.angle + part.angle_speed),
- })))
- }, 70);
-
- const timeout = setTimeout(() => {
- setParticles([]);
- clearInterval(interval);
- }, 5000);
-
- return () => {
- clearInterval(interval);
- clearTimeout(timeout);
- };
- }, [])
-
- const getStyle = (particle: Particle) => ({
- top: `${particle.top}%`,
- left: `${particle.left}%`,
- transform: `rotate(${particle.angle}deg)`,
- width: particle.size,
- height: particle.size,
- })
-
- return
- {particles.map((particle, idx) => )}
-
-}
diff --git a/packages/metavinci/src/views/auctionCreate/index.tsx b/packages/metavinci/src/views/auctionCreate/index.tsx
index 7ae7940..3ec9823 100644
--- a/packages/metavinci/src/views/auctionCreate/index.tsx
+++ b/packages/metavinci/src/views/auctionCreate/index.tsx
@@ -1,9 +1,662 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
+import {
+ Steps,
+ Row,
+ Button,
+ Upload,
+ Col,
+ Input,
+ Statistic,
+ Modal,
+ Progress,
+ Spin,
+ InputNumber,
+ Select,
+} from 'antd';
+import { ArtCard } from './../../components/ArtCard';
+import { UserSearch } from './../../components/UserSearch';
+import { Confetti } from './../../components/Confetti';
+import './../styles.less';
+import { mintNFT } from '../../models';
+import {
+ MAX_METADATA_LEN,
+ MAX_OWNER_LEN,
+ MAX_URI_LENGTH,
+ Metadata,
+ NameSymbolTuple,
+ useConnection,
+ useWallet,
+ IMetadataExtension,
+ MetadataCategory,
+ useConnectionConfig,
+} from '@oyster/common';
+import { getAssetCostToStore, LAMPORT_MULTIPLIER } from '../../utils/assets';
+import { Connection } from '@solana/web3.js';
+import { MintLayout } from '@solana/spl-token';
+import { useHistory, useParams } from 'react-router-dom';
+
+const { Step } = Steps;
+const { Option } = Select;
+const { Dragger } = Upload;
+
+export enum AuctionCategory {
+ Single,
+ Limited,
+ Open,
+ Collection
+}
export const AuctionCreateView = () => {
+ const connection = useConnection();
+ const { env } = useConnectionConfig();
+ const { wallet, connected } = useWallet();
+ const { step_param }: { step_param: string } = useParams()
+ const history = useHistory()
+
+ const [step, setStep] = useState(0);
+ const [saving, setSaving] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [attributes, setAttributes] = useState({
+ name: '',
+ symbol: '',
+ description: '',
+ externalUrl: '',
+ image: '',
+ royalty: 0,
+ files: [],
+ category: MetadataCategory.Image,
+ });
+
+ useEffect(() => {
+ if (step_param) setStep(parseInt(step_param))
+ else gotoStep(0)
+ }, [step_param])
+
+ const gotoStep = (_step: number) => {
+ history.push(`/auction/create/${_step.toString()}`)
+ }
+
+ // store files
+ const mint = async () => {
+ 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 (
-
- TODO: Auction Create view
-
+ <>
+
+ {!saving &&
+
+
+
+
+
+
+ }
+
+ {step === 0 && (
+ {
+ setAttributes({
+ ...attributes,
+ category,
+ });
+ gotoStep(1);
+ }}
+ />
+ )}
+ {step === 1 && (
+ gotoStep(2)}
+ />
+ )}
+
+ {step === 2 && (
+ gotoStep(3)}
+ />
+ )}
+ {step === 3 && (
+ gotoStep(4)}
+ setAttributes={setAttributes}
+ />
+ )}
+ {step === 4 && (
+ gotoStep(5)}
+ connection={connection}
+ />
+ )}
+ {step === 5 && (
+ gotoStep(6)}
+ />
+ )}
+ {step === 6 && (
+
+ )}
+ {(0 < step && step < 5) && gotoStep(step - 1)}>Back }
+
+
+ >
);
};
+
+const CategoryStep = (props: { confirm: (category: MetadataCategory) => void }) => {
+ return (
+ <>
+
+ List an item
+
+ First time listing on Metaplex? Read our sellers' guide.
+
+
+
+
+
+ props.confirm(MetadataCategory.Image)}
+ >
+
+
Single Artwork
+
Sell a one of a kind artwork
+
+
+
+
+ props.confirm(MetadataCategory.Video)}
+ >
+
+
Limited Edition
+
Sell one artwork multiple times
+
+
+
+
+ props.confirm(MetadataCategory.Audio)}
+ >
+
+
Open Edition
+
Sell one artwork with no limit on quantity
+
+
+
+
+ props.confirm(MetadataCategory.Audio)}
+ >
+
+
Collection (Coming Soon)
+
Sell multiple artworks at once
+
+
+
+
+
+ >
+ );
+};
+
+const UploadStep = (props: {
+ attributes: IMetadataExtension;
+ setAttributes: (attr: IMetadataExtension) => void;
+ confirm: () => void;
+}) => {
+ const [mainFile, setMainFile] = useState()
+ const [coverFile, setCoverFile] = useState()
+ const [image, setImage] = useState("")
+
+ useEffect(() => {
+ props.setAttributes({
+ ...props.attributes,
+ files: []
+ })
+ }, [])
+
+ const uploadMsg = (category: MetadataCategory) => {
+ switch (category) {
+ case MetadataCategory.Audio:
+ return "Upload your audio creation (MP3, FLAC, WAV)"
+ case MetadataCategory.Image:
+ return "Upload your image creation (PNG, JPG, GIF)"
+ case MetadataCategory.Video:
+ return "Upload your video creation (MP4)"
+ default:
+ return "Please go back and choose a category"
+ }
+ }
+
+ return (
+ <>
+
+ Now, let's upload your creation
+
+ Your file will be uploaded to the decentralized web via Arweave.
+ Depending on file type, can take up to 1 minute. Arweave is a new type
+ of storage that backs data with sustainable and perpetual endowments,
+ allowing users and developers to truly store data forever – for the
+ very first time.
+
+
+
+ {uploadMsg(props.attributes.category)}
+ {
+ // dont upload files here, handled outside of the control
+ info?.onSuccess?.({}, null as any)
+ }}
+ fileList={mainFile ? [mainFile] : []}
+ onChange={async info => {
+ const file = info.file.originFileObj;
+ if (file) setMainFile(file)
+ if (props.attributes.category != MetadataCategory.Audio) {
+ const reader = new FileReader();
+ reader.onload = function (event) {
+ setImage((event.target?.result as string) || '')
+ }
+ if (file) reader.readAsDataURL(file)
+ }
+ }}
+ >
+
+
Upload your creation
+
+
+ Drag and drop, or click to browse
+
+
+
+ {props.attributes.category == MetadataCategory.Audio &&
+
+ Optionally, you can upload a cover image or video (PNG, JPG, GIF, MP4)
+ {
+ // dont upload files here, handled outside of the control
+ info?.onSuccess?.({}, null as any)
+ }}
+ fileList={coverFile ? [coverFile] : []}
+ onChange={async info => {
+ const file = info.file.originFileObj;
+ if (file) setCoverFile(file)
+ if (props.attributes.category == MetadataCategory.Audio) {
+ const reader = new FileReader();
+ reader.onload = function (event) {
+ setImage((event.target?.result as string) || '')
+ }
+ if (file) reader.readAsDataURL(file)
+ }
+ }}
+ >
+
+
Upload your cover image or video
+
+
+ Drag and drop, or click to browse
+
+
+
+ }
+
+ {
+ props.setAttributes({
+ ...props.attributes,
+ files: [mainFile, coverFile].filter(f => f),
+ image,
+ })
+ props.confirm()
+ }}
+ className="action-btn"
+ >
+ Continue to Mint
+
+
+ >
+ );
+};
+
+const InfoStep = (props: {
+ attributes: IMetadataExtension;
+ setAttributes: (attr: IMetadataExtension) => void;
+ confirm: () => void;
+}) => {
+ return (
+ <>
+
+ Describe your creation
+
+ Provide detailed description of your creative process to engage with
+ your audience.
+
+
+
+
+ {props.attributes.image && (
+
+ )}
+
+
+
+ Title
+
+ props.setAttributes({
+ ...props.attributes,
+ name: info.target.value,
+ })
+ }
+ />
+
+
+ Symbol
+
+ props.setAttributes({
+ ...props.attributes,
+ symbol: info.target.value,
+ })
+ }
+ />
+
+
+ Creators
+
+
+
+ Description
+
+ props.setAttributes({
+ ...props.attributes,
+ description: info.target.value,
+ })
+ }
+ allowClear
+ />
+
+
+
+
+
+ Continue to royalties
+
+
+ >
+ );
+};
+
+const RoyaltiesStep = (props: {
+ attributes: IMetadataExtension;
+ setAttributes: (attr: IMetadataExtension) => void;
+ confirm: () => void;
+}) => {
+ const file = props.attributes.image;
+
+ return (
+ <>
+
+ Set royalties for the creation
+
+ A royalty is a payment made by the seller of this item to the creator.
+ It is charged after every successful auction.
+
+
+
+
+ {file && (
+
+ )}
+
+
+
+ Royalty Percentage
+ {
+ props.setAttributes({ ...props.attributes, royalty: val });
+ }}
+ className="royalties-input"
+ />
+
+
+
+
+
+ Continue to review
+
+
+ >
+ );
+};
+
+const LaunchStep = (props: {
+ confirm: () => void;
+ attributes: IMetadataExtension;
+ connection: Connection;
+}) => {
+ const files = props.attributes.files || [];
+ const metadata = {
+ ...(props.attributes as any),
+ files: files.map(f => f?.name),
+ };
+ const [cost, setCost] = useState(0);
+ useEffect(() => {
+ const rentCall = Promise.all([
+ props.connection.getMinimumBalanceForRentExemption(
+ MintLayout.span,
+ ),
+ props.connection.getMinimumBalanceForRentExemption(
+ MAX_METADATA_LEN,
+ ),
+ props.connection.getMinimumBalanceForRentExemption(
+ MAX_OWNER_LEN,
+ )
+ ]);
+
+ getAssetCostToStore([
+ ...files,
+ new File([JSON.stringify(metadata)], 'metadata.json'),
+ ]).then(async lamports => {
+ const sol = lamports / LAMPORT_MULTIPLIER;
+
+ // TODO: cache this and batch in one call
+ 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 (
+ <>
+
+ Launch your creation
+
+ Provide detailed description of your creative process to engage with
+ your audience.
+
+
+
+
+ {props.attributes.image && (
+
+ )}
+
+
+
+ {cost ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ Pay with SOL
+
+
+ Pay with Credit Card
+
+
+ >
+ );
+};
+
+const WaitingStep = (props: {
+ mint: Function,
+ progress: number,
+ confirm: Function,
+}) => {
+
+ useEffect(() => {
+ const func = async () => {
+ await props.mint()
+ props.confirm()
+ }
+ func()
+ }, [])
+
+ return (
+
+
+
+ Your creation is being uploaded to the decentralized web...
+
+
This can take up to 1 minute.
+
+ )
+}
+
+const Congrats = () => {
+ return <>
+
+
+ Congratulations! Your creation is now live.
+
+
+ Share it on Twitter >
+ See it in your collection >
+ Sell it via auction >
+
+
+
+ >
+}
+
diff --git a/packages/metavinci/src/views/artCreate/styles.less b/packages/metavinci/src/views/styles.less
similarity index 87%
rename from packages/metavinci/src/views/artCreate/styles.less
rename to packages/metavinci/src/views/styles.less
index abd147e..48c0ae9 100644
--- a/packages/metavinci/src/views/artCreate/styles.less
+++ b/packages/metavinci/src/views/styles.less
@@ -60,12 +60,34 @@
}
.type-btn {
- height: 130px;
- width: 240px;
+ height: 80px;
+ width: 280px;
border-radius: 8px;
background-color: #1d1d1d;
border-width: 0px;
- margin-right: 20px;
+ margin-bottom: 20px;
+ text-align: left;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 24px;
+
+ background: linear-gradient(270deg, #616774 7.29%, #403F4C 100%);
+ border-radius: 8px;
+}
+
+.type-btn:hover {
+ background: linear-gradient(270deg, #616774 7.29%, #403F4C 100%) !important;
+}
+.type-btn:disabled {
+ pointer-events: none;
+}
+
+.type-btn-description {
+ color: #aaa;
+ font-size: 0.7rem;
}
.action-btn {