import { useState, DragEvent, useMemo } from 'react'; import Card from '@components/Card'; import styles from '@styles/modules/IPFSUploadForm.module.css'; import UploadIcon from '@icons/Upload'; import CloseWithBGIcon from '@icons/CloseWithBG'; import TextInput from '@components/TextInput'; import AddIcon from '@icons/Add'; import RemoveIcon from '@icons/Remove'; import PrimaryButton from '@components/PrimaryButton'; import { AddResult } from '@node_modules/ipfs-core-types/types/src/root'; import { IPFSHTTPClient } from 'ipfs-http-client'; import { ipfs_client } from '@utils/ipfs'; import Check from '@icons/Check'; import ReadOnly from '@components/ReadOnly'; import HelpText from '@components/HelpText'; import IconButton from './IconButton'; enum ValidFileTypes { 'image/png' = 'image/png', 'image/jpeg' = 'image/jpeg', 'application/json' = 'application/json', } export enum ExtraAttributes { fileSize = 'fileSize', fileType = 'fileType', objectHash = 'objectHash', } export enum MetaDataKeys { traitType = 'trait_type', value = 'value', } export interface MetaData { [MetaDataKeys.traitType]: string; [MetaDataKeys.value]: string; } export interface IpfsMetaObject { name: string; description: string; image: string; attributes: MetaData[]; } const MAX_META_ROWS = 10; const IPFSUploadForm = ({ nextPageHref, setError, }: { nextPageHref: string; setError: (err: string) => void; }) => { const [fileData, setFileData] = useState(null); const [metaName, setMetaName] = useState(''); const [metaDescription, setMetaDescription] = useState(''); const [metaDataList, setMetaDataList] = useState([ { [MetaDataKeys.traitType]: '', [MetaDataKeys.value]: '' }, ]); const [isLoading, setIsLoading] = useState(false); const [metaHash, setMetaHash] = useState(''); const [uploadSuccessful, setUploadSuccessful] = useState(false); const client: IPFSHTTPClient | undefined = useMemo(() => { return ipfs_client(); }, []); const handleFileUpload = async (): Promise => { if (!fileData) { return; } try { setIsLoading(true); const objectResponse = await ipfs_add(fileData); if (!objectResponse) { throw 'Failed to upload object'; } const metaObject: IpfsMetaObject | undefined = buildMetaObject( objectResponse.path ); if (!metaObject) { throw 'Failed to construct meta object'; } const metaResponse = await ipfs_add(JSON.stringify(metaObject)); if (!metaResponse) { throw 'Faild to upload meta object'; } setMetaHash(metaResponse.path); setUploadSuccessful(true); } catch (err) { setIsLoading(false); setUploadSuccessful(false); setError('Unexpected error occured, please try again.'); } }; const ipfs_add = async ( uploadData: File | string ): Promise => { if (client) { try { const response: AddResult = await client.add(uploadData); return response; } catch (err) { console.log(err); } } }; const buildMetaObject = (objectHash: string): IpfsMetaObject | undefined => { if (!fileData) { return; } const fileteredMetadata = metaDataList.filter((el) => { if (el[MetaDataKeys.traitType] !== '' && el[MetaDataKeys.value] !== '') { return { [MetaDataKeys.traitType]: el[MetaDataKeys.traitType].replace( /\s/g, '' ), [MetaDataKeys.value]: el[MetaDataKeys.value], }; } }); const metaObject: IpfsMetaObject = { name: metaName, description: metaDescription, image: `https://cloudflare-ipfs.com/ipfs/${objectHash}`, attributes: [ ...fileteredMetadata, { [MetaDataKeys.traitType]: ExtraAttributes.fileSize, [MetaDataKeys.value]: calculateFileSize(fileData.size), }, { [MetaDataKeys.traitType]: ExtraAttributes.fileType, [MetaDataKeys.value]: ValidFileTypes[fileData.type as ValidFileTypes], }, { [MetaDataKeys.traitType]: ExtraAttributes.objectHash, [MetaDataKeys.value]: objectHash, }, ], }; return metaObject; }; const handleFileDrop = (e: DragEvent): void | undefined => { e.nativeEvent.preventDefault(); if (e.nativeEvent.dataTransfer?.files) { const file = e.nativeEvent.dataTransfer.files[0]; if (!validFile(file.type)) { const errorMsg = 'Attempting to upload an unsupported file type'; setError(errorMsg); return; } setFileData(file); } }; const handleFileSelect = (files: FileList | null) => { if (files) { const file = files[0]; if (!validFile(file.type)) { setError('Attempting to upload an unsupported file type'); return; } setFileData(file); } }; const validFile = (type: string): boolean => { const typeStrings: string[] = Object.values(ValidFileTypes); return typeStrings.includes(type); }; const calculateFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const decimals = 2; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return ( parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i] ); }; const handleFileReset = (): void => { setFileData(null); }; const setMetaItem = ( e: React.FormEvent, index: number, key: MetaDataKeys ): void => { const metaList: MetaData[] = [...metaDataList]; metaList[index][key] = e.currentTarget.value; setMetaDataList(metaList); }; const addSubtractButton = (index: number) => { if (metaDataList.length > 1) { if (index + 1 === metaDataList.length) { return (
removeMetaRow(index)} flat> addMetaRow()} flat>
); } else { return (
removeMetaRow(index)} flat>
); } } else { return (
addMetaRow()} flat>
); } }; const addMetaRow = (): void => { if (metaDataList.length === MAX_META_ROWS) { return setError('Maximum number of meta rows reached'); } const metaList: MetaData[] = [...metaDataList]; metaList.push({ [MetaDataKeys.traitType]: '', [MetaDataKeys.value]: '' }); setError(''); setMetaDataList(metaList); }; const removeMetaRow = (index: number): void => { const metaList: MetaData[] = [...metaDataList]; metaList.splice(index, 1); setMetaDataList(metaList); setError(''); }; const uploadReady = (): boolean => { return !!fileData && metaName !== '' && metaDescription !== ''; }; const imagePreview = (): JSX.Element | undefined => { if (!fileData) { return; } const url = window.URL.createObjectURL(fileData); return (
); }; const uploadForm = (): JSX.Element => { return (

Upload Document

e.preventDefault()} onDrop={(e) => handleFileDrop(e)} > {!fileData && (
e.preventDefault()} onDrop={(e) => handleFileDrop(e)} >

Drag and drop a file to upload

handleFileSelect(e.currentTarget.files)} />

File types supported png, jpg, json

)} {!!fileData && (

File Added

{imagePreview()}

{fileData.name}

{calculateFileSize(fileData.size)}

)}

File Details

setMetaName(e.currentTarget.value)} value={metaName} containerClassName={styles['ipfs__input-row']} labelClassName="bg-[#0E0E0E]" /> setMetaDescription(e.currentTarget.value)} value={metaDescription} containerClassName={styles['ipfs__input-row']} labelClassName="bg-[#0A0A0A]" />

Metadata

{metaDataList.map((data, index) => { return (
setMetaItem(e, index, MetaDataKeys.traitType)} containerClassName={styles['ipfs__meta-input']} labelClassName="bg-[#060606]" /> setMetaItem(e, index, MetaDataKeys.value)} containerClassName={styles['ipfs__meta-input']} labelClassName="bg-[#060606]" /> {addSubtractButton(index)}
); })} Upload
); }; const uploadSuccessView = (): JSX.Element | undefined => { if (!fileData) { setUploadSuccessful(false); return; } const objectUrl = URL.createObjectURL(fileData); return (

Upload Successful

Image

Details

Copy the IPFS hash to view more details on the next step
Next
); }; return {!uploadSuccessful ? uploadForm() : uploadSuccessView()}; }; export default IPFSUploadForm;