diff --git a/src/hooks/use-register-asset.ts b/src/hooks/use-register-asset.ts index e0bfe103..fbf7d800 100644 --- a/src/hooks/use-register-asset.ts +++ b/src/hooks/use-register-asset.ts @@ -63,17 +63,11 @@ export const useRegisterAsset = (): UseRegisterAssetHook => { try { const toAddress = sessionData?.profile?.ownedBy.address; // const asset = BigInt(assetId).toString(10) - const asset = BigInt(assetId) + const asset = assetId.replace(/^f0/, '0x'); if (!toAddress) { throw new Error('The active account address was not found in the session.'); } - console.log('address') - console.log(toAddress) - console.log('asset') - console.log(assetId) - console.log(asset) - const registerAssetData = encodeFunctionData({ abi: AssetOwnershipAbi.abi, functionName: 'registerAsset', diff --git a/src/hooks/use-submit-assets-to-lens.ts b/src/hooks/use-submit-assets-to-lens.ts new file mode 100644 index 00000000..00756033 --- /dev/null +++ b/src/hooks/use-submit-assets-to-lens.ts @@ -0,0 +1,221 @@ +import { useState, useCallback } from "react"; +import { useCreatePost } from "@lens-protocol/react-web"; +import { AnyMedia, MediaVideoMimeType, video } from "@lens-protocol/metadata"; +import { uploadMetadataToIPFS, verifyIpfsData } from "@src/utils/ipfs.ts"; +import uuidv4 from "@src/utils/uuidv4.ts"; +import { ERRORS } from "@notifications/errors.ts"; + +interface SuccessResult { + hash: string; + status: "success"; +} + +interface ErrorResult { + hash: string; + status: "error"; + message: string; +} + +interface UseSubmitAssetToLensReturn { + data: SuccessResult[]; + errors: ErrorResult[]; + loading: boolean; + submitAssetToLens: (hashesString: string) => Promise; +} + +export function useSubmitAssetToLens(): UseSubmitAssetToLensReturn { + const [data, setData] = useState([]); + const [errors, setErrors] = useState([]); + const [loading, setLoading] = useState(false); + const { execute: createPost } = useCreatePost(); + + /** + * Sanitize the description to avoid strange characters + */ + const sanitizeDescription = useCallback((description: any): string => { + if (typeof description !== "string") { + return description; + } + let sanitized = description.replace(/"/g, "'"); + sanitized = sanitized.replace(/\\/g, ""); + // eslint-disable-next-line no-control-regex + sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, ""); + return sanitized; + }, []); + + /** + * Process a single hash + */ + const processHash = useCallback( + async (asset: string): Promise => { + try { + // 1. Get metadata from the endpoint + const response = await fetch(`https://g.watchit.movie/metadata/${asset}/`); + + if (!response.ok) { + return { + hash: asset, + status: "error", + message: `Error fetching metadata: ${response.statusText}`, + }; + } + + const responseData = await response.json(); + const title = responseData.Data.title; + const descriptionRaw = responseData.Data.description; + const description = sanitizeDescription(descriptionRaw); + + // 2. Search for wallpaper and poster/large CIDs + let wallpaperCid = ""; + let largeCid = ""; + + for (const attachment of responseData.Data.attachments) { + if (attachment.title === "wallpaper") { + wallpaperCid = attachment.cid; + } else if (attachment.title === "large") { + largeCid = attachment.cid; + } + } + + if (!wallpaperCid || !largeCid) { + return { + hash: asset, + status: "error", + message: "Missing wallpaper or large CIDs", + }; + } + + const getMediaUri = (cid: string) => `https://g.watchit.movie/content/${cid}/`; + + // 3. Assemble the attachments (media) + const mediaItems: AnyMedia[] = [ + { + item: getMediaUri(largeCid) as any, + type: "image/jpeg" as any, + altTag: "poster" as any, + }, + { + item: getMediaUri(wallpaperCid) as any, + type: "image/png" as any, + altTag: "wallpaper" as any, + }, + ]; + + // 4. Create the metadata for Lens with @lens-protocol/metadata + const metadata = video({ + id: uuidv4(), + title: title, + content: description, + video: { + item: asset, + type: MediaVideoMimeType.MP4, + altTag: "asset", + }, + locale: "en", + attachments: mediaItems, + appId: "watchit", + }); + + // 5. Upload the metadata to IPFS via Pinata + const metadataUri = await uploadMetadataToIPFS(metadata); + + // 6. Verify IPFS data + await verifyIpfsData(metadataUri); + + // 7. Create the post in Lens + const result = await createPost({ + metadata: metadataUri, + }); + + if (result.isFailure()) { + return { + hash: asset, + status: "error", + message: ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR, + }; + } else { + // 8. Wait for the transaction to be mined + const completion = await result.value.waitForCompletion(); + if (completion.isFailure()) { + return { + hash: asset, + status: "error", + message: ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR, + }; + } else { + return { + hash: asset, + status: "success", + }; + } + } + } catch (err: any) { + return { + hash: asset, + status: "error", + message: ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR, + }; + } + }, + [createPost, sanitizeDescription] + ); + + /** + * Main function that processes one or more hashes (separated by commas) + */ + const submitAssetToLens = useCallback( + async (hashesString: string) => { + setLoading(true); + setErrors([]); + setData([]); + + // Separate the string by commas and clean spaces + const hashes = hashesString + .split(",") + .map((h) => h.trim()) + .filter(Boolean); + + // Map each hash to the processing function + const promises = hashes.map((hash) => processHash(hash)); + + // Wait for all promises to settle + const results = await Promise.allSettled(promises); + + // Separate successful and failed results + const successfulResults: SuccessResult[] = []; + const errorResults: ErrorResult[] = []; + + results.forEach((result, index) => { + if (result.status === "fulfilled") { + const res = result.value; + + if (res.status === "success") { + successfulResults.push(res as SuccessResult); + } else { + errorResults.push(res as ErrorResult); + } + } else { + // This shouldn't happen as processHash handles its own errors + errorResults.push({ + hash: hashes[index], + status: "error", + message: "Unexpected error", + }); + } + }); + + // Update state after all hashes are processed + setData(successfulResults); + setErrors(errorResults); + setLoading(false); + }, + [processHash] + ); + + return { + data, + errors, + loading, + submitAssetToLens, + }; +} diff --git a/src/pages/dashboard/marketing.tsx b/src/pages/dashboard/marketing.tsx index 6333ae5e..0a997d1a 100644 --- a/src/pages/dashboard/marketing.tsx +++ b/src/pages/dashboard/marketing.tsx @@ -1,21 +1,23 @@ -import { Helmet } from 'react-helmet-async'; import BlankView from '../../sections/blank/view'; import ComingSoonView from '../../sections/coming-soon/view'; import {useSelector} from "react-redux"; -import {canViewSection} from "@src/pages/dashboard/studio.tsx"; +import {canViewSection} from "@src/pages/dashboard/ownership.tsx"; import Header from "@src/layouts/dashboard/header.tsx"; import HeaderContent from "@src/layouts/dashboard/header-content.tsx"; import MarketingView from "@src/sections/marketing"; +import { GLOBAL_CONSTANTS } from '@src/config-global.ts'; +import { OgMetaTags } from '@src/components/og-meta-tags.tsx'; + // ---------------------------------------------------------------------- export default function ChatPage() { const sessionData = useSelector((state: any) => state.auth.session); return ( - <> - - WatchIt | Marketing - - + { canViewSection(sessionData) ? (<>
@@ -27,6 +29,6 @@ export default function ChatPage() { ) } - + ); } diff --git a/src/pages/dashboard/ownership.tsx b/src/pages/dashboard/ownership.tsx index e26b74da..e57d060e 100644 --- a/src/pages/dashboard/ownership.tsx +++ b/src/pages/dashboard/ownership.tsx @@ -6,11 +6,19 @@ import { OgMetaTags } from '@src/components/og-meta-tags.tsx'; import Header from '@src/layouts/dashboard/header.tsx'; import HeaderContent from '@src/layouts/dashboard/header-content.tsx'; import Ownership from '@src/sections/ownership'; -import { canViewSection } from '@src/pages/dashboard/studio.tsx'; import { useSelector } from 'react-redux'; // ---------------------------------------------------------------------- +export const canViewSection = (sessionData: any): boolean => { + // Allowed profileId to view (temporary) this section + const allowedProfilesId = ['0x0563', '0x050d','0x055c','0x0514', '0x0510']; // Mihail, Carlos, Jacob, Geolffrey and Watchit Open + // Verify if the current profile is allowed to view this section + return allowedProfilesId.includes(sessionData?.profile?.id ?? ''); +} + +// ---------------------------------------------------------------------- + export default function FileManagerPage() { const sessionData = useSelector((state: any) => state.auth.session); diff --git a/src/pages/dashboard/studio.tsx b/src/pages/dashboard/studio.tsx index d68a3d3f..b9048e2d 100644 --- a/src/pages/dashboard/studio.tsx +++ b/src/pages/dashboard/studio.tsx @@ -1,301 +1,27 @@ -import { Helmet } from 'react-helmet-async'; -import { useState } from 'react'; -import { useCreatePost } from '@lens-protocol/react-web'; -import { AnyMedia, MediaVideoMimeType, video } from '@lens-protocol/metadata'; -import uuidv4 from '../../utils/uuidv4'; -import { Grid, Modal } from '@mui/material'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Typography from '@mui/material/Typography'; -import { GLOBAL_CONSTANTS } from '@src/config-global.ts'; -import { verifyIpfsData } from '@src/utils/ipfs.ts'; +// LOCAL IMPORTS import ComingSoonView from '@src/sections/coming-soon/view.tsx'; import BlankView from '@src/sections/blank/view.tsx'; -import Studio from "@src/sections/studio"; -import HeaderContent from "@src/layouts/dashboard/header-content.tsx"; -import Header from "@src/layouts/dashboard/header.tsx"; -import {useSelector} from "react-redux"; - -const hashes = [ - "f0155122018174e2a7079e266bab70f870249dfa50de77bfcd1263a29a7290c9bedad1ba8", - "f01551220b56b4833e0d9329c7d537638d2637edb708fef507f38b4655e29809d87e1995b", - "f0155122015d682b28898978fa6dd47eb7a9a2cd2d78a30e7a0acbf571e8168c360cb0b92", - "f015512207a2907ea5c70af2c9ff9b052cb5daefc0e11b665282a16fc2db5dede02a79f6e", - "f015512201e8464aa98c40972dc425a83fc195f540990cf76aef52b2904fb168265270e6d", - "f015512208af124036cdaefe4f35b21a711ea7cf0e6c09c55fa3efd675e52a422315510ef", - "f015512206dbd9148b1c159ee683ce8cf2c962a5ddcb6ab7277fe78be19e955fcd0d2f9c2", - "f01551220b67f42a603570d5d0561c3322ceb5d71e5103fcdae7534c5d483de9a46865d87", - "f015512207e2c2d391dc615b91c5c690bfc4d541bd8a35faedc382b53c9d4de1e067d2c15", - "f015512204bcb7b84da07b7a2b8166bf1102fe4555d819364c0d08cf482b56332b762c4e2", - "f01551220c6fefd7d77ac2685c1115cac5382546df6f123bcf2bda59180a7950dbffc1bf1", - "f01551220fde28e80c0f19ff9a6427bf73018c110894f5e51de3bc9cf1839c3fd7c9489b0", - "f015512200f140406ed582d0582026849656f2703b4494343a18389d73558017df86127a7", - "f0155122059dddd205ec73c90510eb0d75d40e14cd9cfed565a43cf6f2051aef24cdc006b", - "f01551220c69efd3f75aca1fc69797aea1010a7561df899e7d7ad607623b5b0fa5ac0b67a", - "f0155122099b26b58e4124649fdf7f9c3148ea3b269b5d0ebb296f313cbed60d69f6115af", - "f015512209104b331db20592e00e26d5cb52f6462ee3a091fea64552d0593ee556f15f243", - "f015512208238a1f461b478242306bd7ca8015d2b3f54855a29cfb77d0b2f435a8a64f9c1", - "f0155122036875f8a3877274020ba1128e7a985fec35d10c9bf7e4cdf4c83cc0fcf2bcc57", - "f01551220afff57d83b228ff9f20b8038a42735112ea2850f7982991b8e086293293edcf0", - "f01551220bd11ec24ead942df0c75d22dbf84ea64ef1fabccb766dc504b1496c72652443a", - "f015512207ab25a1ada816dad9e239ce24a220047e8e1f4b8e093214db1708a0b042d938b", - "f01551220619d0e2cfc668425f384659a77352aff3944fac9744a3dff08de666107ba0306", - "f015512206811b048daec7a3aee71ceb640bc7cd999545b3909fa88abb3a3ea40a5f7c3f0", - "f01551220043c25888364b2b97b0ce5d93023a3713ae398618b575313d539bee6a11f0f23", - "f015512207fa97606410713211db6c80a044b0516725106b2c88292af1c246d5c7d53ce53", - "f0155122076a473da4d5ed2d3d37603c64f19b9c31804fa49ddc2967f92f2cf8ab96e99bc", - "f015512207243aa6579e5ee80b3b0f8c5d6a816bf0cd1200b8c1b5a106dd2801f4959d5ce", - "f0155122055ddb2cfd1f7f7c4679edcb65dc183a5e01adcdedc4f1e298f3bd95295b91338", - "f015512207f6735ccc96c7392f59ec5bc7dee9ea9fe71b22200ca88cefa34b5aa99466bbe", - "f01551220ea95ac3fcf67acb02ce24506a87a3c732d8dea7877ac98d5b51cdf4ea2fed69c", - "f01551220a366105090038403a32ad4deb53c11efde198f8e7dea8bbee00201af9f415eb1", - "f01551220f4b60ff0941575eb060e6c01c2933e81a9f57cb7c90eba24adb54bf9035f455e", - "f01551220e9e1c879d970aa8f8e5d3c499117b488111eb0fae360e2cfe29740dd9e6055a8", - "f0155122029806b42c1e6d2bed0b5a87d9b53ced5067f08719e5584f339389c955a816170" -]; - -const uploadToPinata = async (metadata: any) => { - const pinataApiKey = GLOBAL_CONSTANTS.PINATA_API_KEY; - const pinataSecretApiKey = GLOBAL_CONSTANTS.PINATA_SECRET_API_KEY; - - const url = 'https://api.pinata.cloud/pinning/pinJSONToIPFS'; - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'pinata_api_key': pinataApiKey, - 'pinata_secret_api_key': pinataSecretApiKey, - }, - body: JSON.stringify(metadata), - }); - - if (!response.ok) { - throw new Error(`Error uploading to Pinata: ${response.statusText}`); - } - - const data = await response.json(); - - return `ipfs://${data.IpfsHash}`; -}; - -const sanitizeDescription = (description: any): string => { - if (typeof description !== 'string') { - return description; - } - - let sanitized = description.replace(/"/g, "'"); - - sanitized = sanitized.replace(/\\/g, ''); - - sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, ''); - - // sanitized = sanitized.replace(/[^a-zA-Z0-9 .,;:!?'"-]/g, ''); - - return sanitized; -} +import { OgMetaTags } from '@src/components/og-meta-tags.tsx'; +import { GLOBAL_CONSTANTS } from '@src/config-global.ts'; // ---------------------------------------------------------------------- -export const canViewSection = (sessionData: any): boolean => { - // Allowed profileId to view (temporary) this section - const allowedProfilesId = ['0x0563', '0x050d','0x055c','0x0514', '0x0510']; // Russian creator, Carlos, Jacob, Geolffrey and Watchit Open - // Verify if the current profile is allowed to view this section - return allowedProfilesId.includes(sessionData?.profile?.id ?? ''); -} export default function OverviewFilePage() { - const sessionData = useSelector((state: any) => state.auth.session); - - const [open, setOpen] = useState(false); - - const handleOpen = () => setOpen(true); - const handleClose = () => setOpen(false); - - const { execute: createPost } = useCreatePost(); - - const handleSubmitAll = async () => { - try { - if (!sessionData?.authenticated) { - console.error('No active profile found'); - return; - } - - for (const asset of hashes) { - try { - const response = await fetch(`https://g.watchit.movie/metadata/${asset}/`); - - if (!response.ok) { - console.error(`Error fetching metadata for asset ${asset}: ${response.statusText}`); - continue; - } - - const data = await response.json(); - const title = data.Meta.title; - const descriptionRaw = data.Meta.description; - const description = sanitizeDescription(descriptionRaw); - - let wallpaperCid = ''; - let largeCid = ''; - - for (const attachment of data.Attachment) { - if (attachment.title === 'wallpaper') { - wallpaperCid = attachment.cid; - } else if (attachment.title === 'large') { - largeCid = attachment.cid; - } - } - - if (!wallpaperCid || !largeCid) { - console.error(`Poster or wallpaper not found ${asset}`); - continue; - } - - const getMediaUri = (cid: string): string => `https://g.watchit.movie/content/${cid}/`; - - const mediaItems: AnyMedia[] = [ - { - item: getMediaUri(largeCid) as any, - type: 'image/jpeg' as any, - altTag: 'poster' as string, - }, - { - item: getMediaUri(wallpaperCid) as any, - type: 'image/png' as any, - altTag: 'wallpaper' as string, - }, - ]; - - const metadata = video({ - id: uuidv4(), - title: title, - content: description, - video: { - item: `${asset}`, - type: MediaVideoMimeType.MP4, - altTag: 'asset', - }, - locale: 'en', - attachments: mediaItems, - appId: 'watchit', - }); - - // Subir los metadatos a Pinata - const metadataUri = await uploadToPinata(metadata); - - // Verify availability of metadata on IPFS - await verifyIpfsData(metadataUri); - - // Crear el post en Lens - const result = await createPost({ - metadata: metadataUri, - }); - - if (result.isFailure()) { - console.error(`Error creating post for asset ${asset}:`, result.error); - } else { - const completion = await result.value.waitForCompletion(); - if (completion.isFailure()) { - console.error(`Error waiting for completion for asset ${asset}:`, completion.error.message); - } else { - console.log(`Post for asset ${asset} created successfully`); - } - } - } catch (error) { - console.error(`Error processing asset ${asset}:`, error); - // Continue with next asset - } - } - - window.alert('All posts processed'); - handleClose(); - - } catch (error) { - console.error('Error in handleSubmitAll:', error); - } - }; - - // Allowed profileId to view (temporary) this section - - return ( - <> - - WatchIt | Studio - - - {canViewSection(sessionData) ? ( - <> -
- -
- - - ) : ( - - - - )} - - {/**/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* Do you want other people to see your movies?*/} - {/* */} - {/* */} - {/* */} - {/* Upload Movie*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/**/} - - {/**/} - {/* */} - {/* */} - {/* Upload movies*/} - {/* */} - {/* */} - {/* Upload*/} - {/* */} - {/* */} - {/**/} - + + + + + ); } diff --git a/src/sections/ownership/components/ownership-process.tsx b/src/sections/ownership/components/ownership-process.tsx index d0d01f2b..e7452584 100644 --- a/src/sections/ownership/components/ownership-process.tsx +++ b/src/sections/ownership/components/ownership-process.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; // MUI Imports import LoadingButton from '@mui/lab/LoadingButton'; @@ -12,9 +12,12 @@ import OwnershipProcessModal from "@src/components/modal.tsx"; import Ownership from '@src/assets/illustrations/ownership.svg'; import Iconify from '@src/components/iconify'; import { useRegisterAsset } from '@src/hooks/use-register-asset'; -import { notifyError, notifySuccess } from '@notifications/internal-notifications.ts'; +import { notifyError, notifyInfo, notifySuccess } from '@notifications/internal-notifications.ts'; import { SUCCESS } from '@notifications/success.ts'; import { ERRORS } from '@notifications/errors.ts'; +import { INFO } from '@notifications/info.ts'; +import { useSubmitAssetToLens } from '@src/hooks/use-submit-assets-to-lens.ts'; +import NeonPaper from '@src/sections/publication/NeonPaperContainer.tsx'; /** * OwnershipProcess is a React functional component that manages the process of registering ownership. @@ -60,28 +63,52 @@ const OwnershipProcess = () => { const OwnershipProcessContent = ({ onClose }: { onClose: () => void }) => { const [hashes, setHashes] = useState(''); - const { registerAsset, loading, error } = useRegisterAsset(); - - useEffect(() => { - if (!error) return; - - notifyError(ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR); - }, [error]) + const { registerAsset } = useRegisterAsset(); + const { submitAssetToLens } = useSubmitAssetToLens(); + const [isProcessing, setIsProcessing] = useState(false); + const [progress, setProgress] = useState(0); + const hashesArray = hashes.split(',') + .map(h => h.trim()) + .filter(Boolean); const handleRegister = async () => { if (!hashes) return; + + setProgress(1); + setIsProcessing(true); + try { - await registerAsset(hashes); - notifySuccess(SUCCESS.OWNERSHIP_REGISTERED_SUCCESSFULLY); - onClose(); - } catch (e) { + for (const [index, hash] of hashesArray.entries()) { + try { + // 1. Report progress + notifyInfo(INFO.REGISTER_OWNERSHIP_PROGRESS, { + index: index + 1, + total: hashesArray.length, + options: { autoHideDuration: 3000 } + }, '', { autoHideDuration: 3000 }); + setProgress(index + 1); + + // 2. Register ownership (if it fails, it does not continue) + await registerAsset(hash); + + // 3. Upload to Lens only if registration was successful + await submitAssetToLens(hash); + + notifySuccess(SUCCESS.OWNERSHIP_REGISTERED_SUCCESSFULLY, { count: index + 1 }); + } catch (error) { + notifyError(ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR, { hash: `${index + 1}/${hashesArray.length}` }); + continue; // Continue with the next hash + } + } + } catch (error) { notifyError(ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR); + } finally { + setIsProcessing(false); + onClose(); } }; - const handleHashesChange = (event: React.ChangeEvent) => { - setHashes(event.target.value); - }; + const RainbowEffect = isProcessing ? NeonPaper : Box; return ( @@ -100,25 +127,39 @@ const OwnershipProcessContent = ({ onClose }: { onClose: () => void }) => { label="Content Hash(es)" type="text" value={hashes} - onChange={handleHashesChange} + onChange={(e) => setHashes(e.target.value)} placeholder="e.g., hash1,hash2,hash3" + disabled={isProcessing} /> - - } - disabled={!hashes || loading} - loading={loading} + - Register - + } + > + {isProcessing ? `Processing... (${progress}/${hashesArray.length})` : 'Start Process'} + + ); diff --git a/src/sections/studio/components/studio-process.tsx b/src/sections/studio/components/studio-process.tsx index 69755a32..39ab7952 100644 --- a/src/sections/studio/components/studio-process.tsx +++ b/src/sections/studio/components/studio-process.tsx @@ -1,24 +1,21 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; // @MUI -import LoadingButton from '@mui/lab/LoadingButton'; -import { Stack, Box, Typography, TextField, Button } from '@mui/material'; +import LoadingButton from "@mui/lab/LoadingButton"; +import { Stack, Box, Typography, TextField, Button } from "@mui/material"; // Project Imports import { useBoolean } from "@src/hooks/use-boolean.ts"; -import ProcessSectionCard from '@src/components/process-section-card.tsx'; +import ProcessSectionCard from "@src/components/process-section-card.tsx"; import StudioProcessModal from "@src/components/modal.tsx"; // @ts-ignore -import Process from '@src/assets/illustrations/process.svg'; -import Iconify from '@src/components/iconify'; +import Process from "@src/assets/illustrations/process.svg"; +import Iconify from "@src/components/iconify"; + +// Hook Import +import { useSubmitAssetToLens } from "@src/hooks/use-submit-assets-to-lens"; +import { notifyError, notifySuccess } from '@notifications/internal-notifications.ts'; +import { SUCCESS } from '@notifications/success.ts'; -/** - * `StudioProcess` is a React functional component that provides a user interface for publishing content. - * It includes a primary call-to-action button and a modal for confirmation of publishing actions. - * - * The component uses the `useBoolean` hook to manage the modal's open/close state: - * - Clicking the primary button triggers the display of the modal. - * - Closing the modal resets the state. - */ const StudioProcess = () => { const confirmPublish = useBoolean(); @@ -51,23 +48,41 @@ const StudioProcess = () => { }; const ProcessContent = ({ onClose }: { onClose: () => void }) => { - const [loading, setLoading] = useState(false); - const [hashes, setHashes] = useState(''); + const [hashes, setHashes] = useState(""); + const { data, errors, loading, submitAssetToLens } = useSubmitAssetToLens(); + + const handleHashesChange = (event: React.ChangeEvent) => { + setHashes(event.target.value); + }; - const handleProcess = () => { - setLoading(true); - setTimeout(() => { - setLoading(false); - }, 2000); + const handleProcess = async () => { + await submitAssetToLens(hashes); }; - const handleHashesChange = (_event: React.ChangeEvent) => { - setHashes(_event.target.value); - } + useEffect(() => { + if (errors.length > 0) { + notifyError(errors[0]?.message as any); + } + }, [errors]); + + useEffect(() => { + if (data.length > 0) { + notifySuccess(SUCCESS.OWNERSHIP_REGISTERED_SUCCESSFULLY); + onClose(); + } + }, [data]); return ( - - + + Enter the hash of the content you want to publish @@ -76,7 +91,7 @@ const ProcessContent = ({ onClose }: { onClose: () => void }) => { void }) => { placeholder="Enter the hash (or hashes) of the content" /> + - void }) => { onClick={handleProcess} startIcon={} disabled={!hashes || loading} + loading={loading} > Publish - ) -} + ); +}; export default StudioProcess; diff --git a/src/utils/ipfs.ts b/src/utils/ipfs.ts index c2beb625..4cbf852a 100644 --- a/src/utils/ipfs.ts +++ b/src/utils/ipfs.ts @@ -98,8 +98,7 @@ export const uploadImagesToIPFS = async ( */ export const uploadMetadataToIPFS = async (metadata: any): Promise => { try { - const metadataURI = await uploadToIPFS(metadata); - return metadataURI; + return await uploadToIPFS(metadata); } catch (error) { console.error('Error uploading metadata to IPFS:', error); throw error; diff --git a/src/utils/notifications/errors.ts b/src/utils/notifications/errors.ts index 5f9774cf..f19117e8 100644 --- a/src/utils/notifications/errors.ts +++ b/src/utils/notifications/errors.ts @@ -56,6 +56,9 @@ export enum ERRORS { INVITATION_SEND_ERROR = 'INVITATION_SEND_ERROR', INVITATION_USER_ALREADY_INVITED = 'INVITATION_USER_ALREADY_INVITED', INVITATION_USER_CANT_INVITE_SELF = 'INVITATION_USER_CANT_INVITE_SELF', + + // SUBMIT + SUBMIT_ASSET_ERROR = 'SUBMIT_ASSET_ERROR', } /** @@ -74,7 +77,7 @@ export const ERROR_MESSAGES: Record = { [ERRORS.FAILED_CHANGE_WALLET_ERROR]: 'Error while trying to change the wallet.', [ERRORS.CREATING_PROFILE_ERROR]: 'Error creating profile.', [ERRORS.UPDATING_PROFILE_ERROR]: 'Error updating profile metadata.', - [ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR]: 'An error occurred while registering your asset. Please try again.', + [ERRORS.ASSET_OWNERSHIP_REGISTER_ERROR]: 'Error processing hash: {hash}', [ERRORS.BROADCASTING_TRANSACTION_ERROR]: 'There was an error broadcasting the transaction.', [ERRORS.PENDING_SIGNING_REQUEST_ERROR]: 'There is a pending signing request in your wallet.', @@ -83,6 +86,7 @@ export const ERROR_MESSAGES: Record = { [ERRORS.INSUFFICIENT_FUNDS_ERROR]: 'You do not have enough funds to pay for this follow fee {symbol} {amount}', [ERRORS.WALLET_CONNECTION_ERROR]: 'There was an error connecting to your wallet.', + [ERRORS.SUBMIT_ASSET_ERROR]: 'There was an error submitting your asset.', [ERRORS.PREMATURE_ACTION_ERROR]: 'There is a pending unfollow request for this profile.', // Login error diff --git a/src/utils/notifications/info.ts b/src/utils/notifications/info.ts index 50d670eb..e3ea762c 100644 --- a/src/utils/notifications/info.ts +++ b/src/utils/notifications/info.ts @@ -9,6 +9,7 @@ export enum INFO { APPROVE_WAITING_CONFIRMATION = 'APPROVE_WAITING_CONFIRMATION', DEPOSIT_SENDING_CONFIRMATION = 'DEPOSIT_SENDING_CONFIRMATION', DEPOSIT_WAITING_CONFIRMATION = 'DEPOSIT_WAITING_CONFIRMATION', + REGISTER_OWNERSHIP_PROGRESS = 'REGISTER_OWNERSHIP_PROGRESS', } /** @@ -26,4 +27,5 @@ export const INFO_MESSAGES: Record = { 'Approve transaction broadcasted. Waiting for confirmation (1/2)...', [INFO.DEPOSIT_WAITING_CONFIRMATION]: 'Deposit transaction broadcasted. Waiting for confirmation (2/2)...', + [INFO.REGISTER_OWNERSHIP_PROGRESS]: 'Processing asset {index} of {total}', }; diff --git a/src/utils/notifications/success.ts b/src/utils/notifications/success.ts index dcc1d6c2..44760a10 100644 --- a/src/utils/notifications/success.ts +++ b/src/utils/notifications/success.ts @@ -38,7 +38,7 @@ export const SUCCESS_MESSAGES: Record = { [SUCCESS.TRANSFER_CREATED_SUCCESSFULLY]: 'Transfer sent to {destination}.', [SUCCESS.FOLLOW_UNFOLLOW_SUCCESSFULLY]: 'Successfully {profileName} {actionLbl}.', [SUCCESS.PROFILE_JOINED_SUCCESSFULLY]: 'Successfully joined the profile.', - [SUCCESS.OWNERSHIP_REGISTERED_SUCCESSFULLY]: 'Your ownership has been registered successfully!', + [SUCCESS.OWNERSHIP_REGISTERED_SUCCESSFULLY]: 'Asset {count} successfully processed', // Metamask [SUCCESS.METAMASK_CONNECTED_SUCCESSFULLY]: 'MetaMask connected successfully!',