diff --git a/.env.development b/.env.development index d79e2caa5..4bbe93769 100644 --- a/.env.development +++ b/.env.development @@ -18,4 +18,5 @@ VITE_EVM_PROVIDER_URL=https://rpc.nice.hydration.cloud VITE_EVM_EXPLORER_URL=https://explorer.nice.hydration.cloud VITE_EVM_NATIVE_ASSET_ID=20 VITE_MIGRATION_TRIGGER_DOMAIN="deploy-preview-1334--testnet-hydra-app.netlify.app" -VITE_MIGRATION_TARGET_DOMAIN="testnet-app.hydradx.io" \ No newline at end of file +VITE_MIGRATION_TARGET_DOMAIN="testnet-app.hydradx.io" +VITE_MEMEPAD_APILLON_BUCKET_UUID="1b216a6c-704f-49c4-b5d7-6ae4a16e6f25" \ No newline at end of file diff --git a/.env.production b/.env.production index 79873101a..08e94a23c 100644 --- a/.env.production +++ b/.env.production @@ -18,4 +18,5 @@ VITE_EVM_PROVIDER_URL=https://rpc.hydradx.cloud VITE_EVM_EXPLORER_URL=https://explorer.evm.hydration.cloud VITE_EVM_NATIVE_ASSET_ID=20 VITE_MIGRATION_TRIGGER_DOMAIN="app.hydradx.io" -VITE_MIGRATION_TARGET_DOMAIN="app.hydration.net" \ No newline at end of file +VITE_MIGRATION_TARGET_DOMAIN="app.hydration.net" +VITE_MEMEPAD_APILLON_BUCKET_UUID="1b216a6c-704f-49c4-b5d7-6ae4a16e6f25" \ No newline at end of file diff --git a/.env.rococo b/.env.rococo index 22c397474..e4931775d 100644 --- a/.env.rococo +++ b/.env.rococo @@ -18,4 +18,5 @@ VITE_EVM_PROVIDER_URL= VITE_EVM_EXPLORER_URL= VITE_EVM_NATIVE_ASSET_ID=20 VITE_MIGRATION_TRIGGER_DOMAIN="" -VITE_MIGRATION_TARGET_DOMAIN="" \ No newline at end of file +VITE_MIGRATION_TARGET_DOMAIN="" +VITE_MEMEPAD_APILLON_BUCKET_UUID="1b216a6c-704f-49c4-b5d7-6ae4a16e6f25" \ No newline at end of file diff --git a/src/api/external/assethub.ts b/src/api/external/assethub.ts index e27a012d8..04a2b31c5 100644 --- a/src/api/external/assethub.ts +++ b/src/api/external/assethub.ts @@ -60,6 +60,18 @@ export const assethubNativeToken = assethub.assetsData.get( "dot", ) as ParachainAssetsData +// TEMP CHOPSTICKS SETUP +if (window.location.hostname === "localhost") { + //@ts-ignore + assethub.ws = "ws://172.25.126.217:8000" + const hydradx = chainsMap.get("hydradx") as Parachain + //@ts-ignore + hydradx.ws = "ws://172.25.126.217:8001" + const polkadot = chainsMap.get("polkadot") as Parachain + //@ts-ignore + polkadot.ws = "ws://172.25.126.217:8002" +} + export const getAssetHubAssets = async (api: ApiPromise) => { try { const [dataRaw, assetsRaw] = await Promise.all([ diff --git a/src/components/FileUploader/FileUploader.styled.ts b/src/components/FileUploader/FileUploader.styled.ts index 13cdaeb3e..dd89871ca 100644 --- a/src/components/FileUploader/FileUploader.styled.ts +++ b/src/components/FileUploader/FileUploader.styled.ts @@ -17,6 +17,10 @@ export const SContainer = styled.div<{ error?: boolean }>` ${({ error }) => (error ? theme.colors.red400 : theme.colors.basic600)}; border-radius: ${theme.borderRadius.default}px; + &:hover { + border-color: ${theme.colors.brightBlue300}; + } + &.drag-over { border-color: ${theme.colors.brightBlue300}; background-color: ${theme.colors.darkBlue401}; @@ -46,16 +50,26 @@ export const SUploadPreview = styled.div` ` export const SClearButton = styled.button` - background-color: ${theme.colors.basic800}; - border: none; position: absolute; top: 4px; right: 4px; + + display: inline-flex; + justify-content: center; + align-items: center; + + width: 24px; + height: 24px; + padding: 4px; + color: ${theme.colors.basic400}; - cursor: pointer; + background-color: ${theme.colors.basic800}; + border: none; border-radius: ${theme.borderRadius.default}px; + cursor: pointer; + :hover, :focus { background-color: ${theme.colors.basic700}; diff --git a/src/components/FileUploader/FileUploader.tsx b/src/components/FileUploader/FileUploader.tsx index 3e06b8ecc..d83263856 100644 --- a/src/components/FileUploader/FileUploader.tsx +++ b/src/components/FileUploader/FileUploader.tsx @@ -1,9 +1,15 @@ import { asUploadButton } from "@rpldy/upload-button" import UploadDropZone from "@rpldy/upload-drop-zone" -import UploadPreview, { PreviewMethods } from "@rpldy/upload-preview" -import Uploady, { BatchItem, UPLOADER_EVENTS, useUploady } from "@rpldy/uploady" +import Uploady, { Batch, UPLOADER_EVENTS, useUploady } from "@rpldy/uploady" +import CrossIcon from "assets/icons/CrossIcon.svg?react" import { Text } from "components/Typography/Text/Text" -import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react" +import React, { + forwardRef, + useCallback, + useEffect, + useMemo, + useState, +} from "react" import { useTranslation } from "react-i18next" import { SClearButton, @@ -21,9 +27,11 @@ import { DEFAULT_MIN_WIDTH, FileError, FileType, + isFile, parseDimensions, useFileErrorMessage, } from "./FileUploader.utils" +import { usePrevious } from "react-use" export type FileUploaderProps = { placeholder?: string @@ -31,97 +39,32 @@ export type FileUploaderProps = { maxDimensions?: string maxSize?: number allowedTypes?: readonly FileType[] + onChange?: (files: File[]) => void } +export const FileUploaderProvider = Uploady + const UploadButton = asUploadButton( forwardRef((props, ref) => { return }), ) -type UploaderWrapperProps = Partial & { - file: BatchItem | null - error?: string - onFileAdded?: (item: BatchItem) => void - onFileRemoved?: () => void - previewMethodsRef: React.MutableRefObject -} - -const UploaderWrapper: React.FC = ({ - placeholder, - error, - file, - allowedTypes, - onFileAdded, - onFileRemoved, - previewMethodsRef, -}) => { - const uploady = useUploady() - - useEffect(() => { - const handleItemStart = (item: BatchItem) => onFileAdded?.(item) - uploady.on(UPLOADER_EVENTS.ITEM_START, handleItemStart) - return () => { - uploady.off(UPLOADER_EVENTS.ITEM_START, handleItemStart) - } - }, [onFileAdded, uploady]) - - function clearPreview() { - previewMethodsRef.current?.clear() - onFileRemoved?.() - } - - const hasFile = !!file - - return ( - - - - <> - {!hasFile && ( - <> - - {placeholder} - - {!error && allowedTypes && ( - - ({allowedTypes.join(", ")}) - - )} - - )} - {error && ( - - {error} - - )} - - - - - {hasFile && ( - - x - - )} - - - - ) -} - export const FileUploader: React.FC = ({ placeholder, minDimensions = `${DEFAULT_MIN_WIDTH}x${DEFAULT_MIN_HEIGHT}`, maxDimensions = `${DEFAULT_MAX_WIDTH}x${DEFAULT_MAX_HEIGHT}`, maxSize = DEFAULT_MAX_SIZE, allowedTypes = ALL_FILE_TYPES, + onChange, }) => { const { t } = useTranslation() - const [file, setFile] = useState(null) + const { abortBatch, clearPending, ...uploady } = useUploady() + + const [batch, setBatch] = useState(null) const [errorCode, setErrorCode] = useState(null) - const previewMethodsRef = useRef(null) + const prevBatch = usePrevious(batch) const [minWidth, minHeight] = parseDimensions(minDimensions) const [maxWidth, maxHeight] = parseDimensions(maxDimensions) @@ -135,12 +78,49 @@ export const FileUploader: React.FC = ({ maxHeight, allowedTypes, onError: (error) => { + if (batch) abortBatch(batch.id) setErrorCode(error.code) - setFile(null) - previewMethodsRef.current?.clear() }, }) - }, [allowedTypes, maxHeight, maxSize, maxWidth, minHeight, minWidth]) + }, [ + abortBatch, + allowedTypes, + batch, + maxHeight, + maxSize, + maxWidth, + minHeight, + minWidth, + ]) + + const onBatchAdded = useCallback( + (batch: Batch) => { + if (prevBatch) abortBatch(prevBatch.id) + setBatch(batch) + setErrorCode(null) + + const files = batch.items.map(({ file }) => file).filter(isFile) + onChange?.(files) + }, + [abortBatch, onChange, prevBatch], + ) + + const onBatchAborted = useCallback(() => { + setBatch?.(null) + onChange?.([]) + }, [onChange]) + + useEffect(() => { + uploady.on(UPLOADER_EVENTS.BATCH_ADD, onBatchAdded) + uploady.on(UPLOADER_EVENTS.BATCH_ABORT, onBatchAborted) + uploady.on(UPLOADER_EVENTS.BATCH_CANCEL, onBatchAborted) + + return () => { + uploady.off(UPLOADER_EVENTS.BATCH_ADD, onBatchAdded) + uploady.off(UPLOADER_EVENTS.BATCH_ABORT, onBatchAborted) + uploady.off(UPLOADER_EVENTS.BATCH_CANCEL, onBatchAborted) + } + }, [onBatchAborted, onBatchAdded, uploady]) const error = useFileErrorMessage(errorCode, { maxSize, @@ -151,20 +131,47 @@ export const FileUploader: React.FC = ({ allowedTypes, }) + const files = batch?.items + ? batch.items.map(({ file }) => file).filter(isFile) + : [] + + console.log({ batch }) + return ( - - { - setFile(file) - setErrorCode(null) - }} - onFileRemoved={() => setFile(null)} - previewMethodsRef={previewMethodsRef} - /> - + + + + <> + {!batch && ( + <> + + {placeholder ?? t("fileUploader.placeholder")} + + {!error && allowedTypes && ( + + ({allowedTypes.join(", ")}) + + )} + + )} + {error && ( + + {error} + + )} + + + + {files.map((file) => ( + + ))} + {batch && ( + abortBatch(batch.id)}> + + + )} + + + ) } diff --git a/src/components/FileUploader/FileUploader.utils.ts b/src/components/FileUploader/FileUploader.utils.ts index 9c2fad985..3a62b8049 100644 --- a/src/components/FileUploader/FileUploader.utils.ts +++ b/src/components/FileUploader/FileUploader.utils.ts @@ -1,5 +1,11 @@ -import { FileFilterMethod } from "@rpldy/uploady" -import { useMemo } from "react" +import { + Batch, + FileFilterMethod, + UPLOADER_EVENTS, + UploadOptions, + useUploady, +} from "@rpldy/uploady" +import { useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" export const ALL_FILE_TYPES = ["png", "jpg", "svg", "webp"] as const @@ -85,7 +91,7 @@ const isFileSizeValid = (file: File, maxSize: number = DEFAULT_MAX_SIZE) => { return false } -const isFileDimensionsValid = async ( +const isImageDimensionsValid = async ( file: File, { minWidth = DEFAULT_MIN_WIDTH, @@ -95,6 +101,9 @@ const isFileDimensionsValid = async ( }, ) => { if (file instanceof File) { + // skip of MIME type is not an image + if (!file.type.startsWith("image/")) return true + const dimensions = await getFileDimensions(file) if (!dimensions) return false @@ -141,7 +150,7 @@ export const createFileFilter = ({ } if ( - !(await isFileDimensionsValid(file, { + !(await isImageDimensionsValid(file, { minWidth, maxWidth, minHeight, @@ -205,6 +214,33 @@ export const useFileErrorMessage = ( ]) } +export const useUploadPendingFiles = () => { + const uploady = useUploady() + + return useCallback( + async (options: UploadOptions) => { + return new Promise((resolve, reject) => { + uploady.processPending(options) + + const onProgress = (batch: Batch) => { + if (batch.completed === 100) { + resolve() + } + } + + uploady.on(UPLOADER_EVENTS.BATCH_PROGRESS, onProgress) + uploady.on(UPLOADER_EVENTS.BATCH_ERROR, reject) + + uploady.once(UPLOADER_EVENTS.BATCH_FINALIZE, () => { + uploady.off(UPLOADER_EVENTS.BATCH_PROGRESS, onProgress) + uploady.off(UPLOADER_EVENTS.BATCH_ERROR, reject) + }) + }) + }, + [uploady], + ) +} + export const parseDimensions = (dimensions: string) => { try { const [width, height] = dimensions.toLowerCase().split("x").map(Number) @@ -221,3 +257,5 @@ export const formatBytes = (bytes: number) => { const i = Math.floor(Math.log(bytes) / Math.log(1024)) return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}` } + +export const isFile = (value: unknown): value is File => value instanceof File diff --git a/src/components/FileUploader/index.ts b/src/components/FileUploader/index.ts index 1afd7ce65..1dbc61041 100644 --- a/src/components/FileUploader/index.ts +++ b/src/components/FileUploader/index.ts @@ -1 +1,2 @@ export * from "./FileUploader" +export * from "./FileUploader.utils" diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 2f270c3c5..858e94f26 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -1008,7 +1008,7 @@ "memepad.form.symbol.placeholder": "Ex. NOFUN", "memepad.form.decimals": "Decimals", "memepad.form.supply": "Total supply", - "memepad.form.supply.placeholder": "EX. 32 000 000", + "memepad.form.supply.placeholder": "Ex. 32 000 000", "memepad.form.allocatedSupply": "Deposit initial supply", "memepad.form.deposit": "Existential deposit", "memepad.form.account": "Creator account", diff --git a/src/sections/memepad/MemepadPage.tsx b/src/sections/memepad/MemepadPage.tsx index 410faaacd..263174e66 100644 --- a/src/sections/memepad/MemepadPage.tsx +++ b/src/sections/memepad/MemepadPage.tsx @@ -69,8 +69,6 @@ const MemepadPageContent = () => { } export const MemepadPage = () => { - const { account } = useAccount() - const [degenModalOpen, setDegenModalOpen] = useState(false) const { degenMode, toggleDegenMode } = useSettingsStore() const initialDegenModeState = useRef(degenMode) @@ -89,7 +87,7 @@ export const MemepadPage = () => { return ( <> - + diff --git a/src/sections/memepad/components/MemepadSummary/MemepadSummary.tsx b/src/sections/memepad/components/MemepadSummary/MemepadSummary.tsx index e160fad89..a799680df 100644 --- a/src/sections/memepad/components/MemepadSummary/MemepadSummary.tsx +++ b/src/sections/memepad/components/MemepadSummary/MemepadSummary.tsx @@ -67,6 +67,7 @@ export const MemepadSummary: React.FC = ({ if (!values) return null const { + file, symbol, name, decimals, @@ -127,6 +128,19 @@ export const MemepadSummary: React.FC = ({ {t("memepad.summary.yourSummary")}: + {file instanceof File && ( + + + Logo + + {name} + + )} {t("memepad.form.name")} @@ -189,7 +203,16 @@ export const MemepadSummary: React.FC = ({
- } /> + + ) : ( + + ) + } + /> } diff --git a/src/sections/memepad/form/MemepadForm.utils.tsx b/src/sections/memepad/form/MemepadForm.utils.tsx index 8d461b113..309c82e05 100644 --- a/src/sections/memepad/form/MemepadForm.utils.tsx +++ b/src/sections/memepad/form/MemepadForm.utils.tsx @@ -37,6 +37,7 @@ import { QUERY_KEYS } from "utils/queryKeys" import { noWhitespace, positive, required } from "utils/validators" import { z } from "zod" import { MemepadFormFields } from "./MemepadFormFields" +import { useUploadPendingFiles } from "components/FileUploader" export const MEMEPAD_XCM_RELAY_CHAIN = "polkadot" export const MEMEPAD_XCM_SRC_CHAIN = "assethub" @@ -61,7 +62,10 @@ const DEFAULT_DECIMALS_COUNT = 12 const DEFAULT_EXISTENTIAL_DEPOSIT = 1 const DEFAULT_DOT_SUPPLY = 10 +const APILLON_BUCKET_UUID = import.meta.env.VITE_MEMEPAD_APILLON_BUCKET_UUID + export type MemepadFormValues = CreateTokenValues & { + file: File | null internalId: string xykPoolAssetId: string xykPoolSupply: string @@ -105,6 +109,7 @@ export const useMemepadForm = ({ id: "", name: "", symbol: "", + file: null, deposit: DEFAULT_EXISTENTIAL_DEPOSIT.toString(), supply: "", allocatedSupply: "", @@ -223,6 +228,7 @@ export const useMemepad = () => { const [step, setStep] = useState(MemepadStep.CREATE_TOKEN) const [supplyPerc, setSupplyPerc] = useState(50) const dotTransferredRef = useRef(false) + const uploadPendingFiles = useUploadPendingFiles() const { addToken } = useUserExternalTokenStore() const refetchProvider = useRefetchProviderData() @@ -314,6 +320,20 @@ export const useMemepad = () => { syncAssethubXcmConfig(registeredAsset) form.setValue("internalId", internalId) + + if (formValues.file instanceof File) { + const fileExt = formValues.file.name.split(".").pop() + const fileName = `${formValues.origin}_${id}_${internalId}.${fileExt}` + await uploadPendingFiles({ + destination: { + url: `http://localhost:3000/api/apillon/bucket/${APILLON_BUCKET_UUID}/upload`, + params: { + fileName, + }, + }, + }) + } + setNextStep() currentStep++ } diff --git a/src/sections/memepad/form/MemepadFormContext.tsx b/src/sections/memepad/form/MemepadFormContext.tsx index 1e288c373..286231f11 100644 --- a/src/sections/memepad/form/MemepadFormContext.tsx +++ b/src/sections/memepad/form/MemepadFormContext.tsx @@ -1,10 +1,7 @@ -import { createContext, useContext } from "react" +import { FileUploaderProvider } from "components/FileUploader" +import { createContext, PropsWithChildren, useContext } from "react" import { useMemepad } from "./MemepadForm.utils" -export type MemepadFormContextProps = { - children?: React.ReactNode -} - type FormContext = ReturnType const MemepadFormContext = createContext({} as FormContext) @@ -13,7 +10,7 @@ export const useMemepadFormContext = () => { return useContext(MemepadFormContext) } -export const MemepadFormProvider: React.FC = ({ +export const MemepadFormContextProvider: React.FC = ({ children, }) => { const value = useMemepad() @@ -23,3 +20,11 @@ export const MemepadFormProvider: React.FC = ({ ) } + +export const MemepadFormProvider: React.FC = ({ + children, +}) => ( + + {children} + +) diff --git a/src/sections/memepad/form/MemepadFormFields.tsx b/src/sections/memepad/form/MemepadFormFields.tsx index 44baa6119..6ec1d4cdf 100644 --- a/src/sections/memepad/form/MemepadFormFields.tsx +++ b/src/sections/memepad/form/MemepadFormFields.tsx @@ -95,10 +95,21 @@ export const MemepadFormFields: FC = ({ form }) => { type="hidden" {...form.register("decimals", { valueAsNumber: true })} /> - ( + { + const file = files?.[0] || null + field.onChange(file) + }} + /> + )} />