From ce6abd775a3f675aa774cdaab3735433b2ed23e2 Mon Sep 17 00:00:00 2001 From: shixuewen Date: Thu, 8 Feb 2024 16:54:33 +0800 Subject: [PATCH 1/8] feat: red envelope --- apps/u3/src/components/layout/Index.tsx | 2 + .../frames/red-envelope/CreateFrameForm.tsx | 175 +++++++++++++++ .../frames/red-envelope/CreateModal.tsx | 148 +++++++++++++ .../frames/red-envelope/EmbedFramePreview.tsx | 28 +++ .../frames/red-envelope/PostFrameForm.tsx | 202 ++++++++++++++++++ .../RedEnvelopeFloatingWindow.tsx | 45 ++++ 6 files changed, 600 insertions(+) create mode 100644 apps/u3/src/components/social/frames/red-envelope/CreateFrameForm.tsx create mode 100644 apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx create mode 100644 apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx create mode 100644 apps/u3/src/components/social/frames/red-envelope/PostFrameForm.tsx create mode 100644 apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx diff --git a/apps/u3/src/components/layout/Index.tsx b/apps/u3/src/components/layout/Index.tsx index 41df61ab..80fe79da 100644 --- a/apps/u3/src/components/layout/Index.tsx +++ b/apps/u3/src/components/layout/Index.tsx @@ -21,6 +21,7 @@ import MobileNav from './mobile/MobileNav'; import { MobileGuide } from './mobile/MobileGuide'; import AddPostMobile from '../social/AddPostMobile'; import ClaimOnboard from '../onboard/Claim'; +import RedEnvelopeFloatingWindow from '../social/frames/red-envelope/RedEnvelopeFloatingWindow'; function Layout() { const { ready } = useAuthentication(); @@ -55,6 +56,7 @@ function Layout() { )} + )} diff --git a/apps/u3/src/components/social/frames/red-envelope/CreateFrameForm.tsx b/apps/u3/src/components/social/frames/red-envelope/CreateFrameForm.tsx new file mode 100644 index 00000000..2d952ff5 --- /dev/null +++ b/apps/u3/src/components/social/frames/red-envelope/CreateFrameForm.tsx @@ -0,0 +1,175 @@ +import { ComponentPropsWithRef } from 'react'; +import { toast } from 'react-toastify'; +import { Checkbox } from '@/components/ui/checkbox'; +import { cn } from '@/lib/utils'; + +export const constraintsOptions = [ + { + value: 'Follow', + label: 'Follow', + }, + { + value: 'Like', + label: 'Like', + }, + { + value: 'Repost', + label: 'Repost', + }, +]; + +export type FrameFormValues = { + constraints: string[]; + singleMinReward: number; + singleMaxReward: number; + totalReward: number; +}; + +export const defaultFrameFormValues: FrameFormValues = { + constraints: [], + singleMinReward: 0, + singleMaxReward: 0, + totalReward: 0, +}; + +export type CreateFrameFormProps = ComponentPropsWithRef<'form'> & { + values: FrameFormValues; + disabled?: boolean; + onValuesChange: (values: FrameFormValues) => void; + onSubmit: (values: FrameFormValues) => void; +}; + +export default function CreateFrameForm({ + values, + disabled, + onValuesChange, + onSubmit, + className, + ...props +}: CreateFrameFormProps) { + const { constraints, singleMinReward, singleMaxReward, totalReward } = values; + const constraintsOptionsEl = constraintsOptions.map(({ value, label }) => ( +
+ { + if (v) { + onValuesChange({ ...values, constraints: [...constraints, value] }); + } else { + onValuesChange({ + ...values, + constraints: constraints.filter((c) => c !== value), + }); + } + }} + /> + +
+ )); + return ( +
+
+ + Constraints + +
+ {constraintsOptionsEl} +
+
+
+ + Random interval + +
+
+ { + onValuesChange({ ...values, singleMinReward: +e.target.value }); + }} + /> + $DEGEN +
+
+
+ { + onValuesChange({ ...values, singleMaxReward: +e.target.value }); + }} + /> + $DEGEN +
+
+
+
+ Reward +
+
+ { + onValuesChange({ ...values, totalReward: +e.target.value }); + }} + /> + $DEGEN +
+ +
+
+
+ ); +} diff --git a/apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx b/apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx new file mode 100644 index 00000000..5eeeb18e --- /dev/null +++ b/apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx @@ -0,0 +1,148 @@ +import { useEffect, useState } from 'react'; +import ModalContainer from '@/components/common/modal/ModalContainer'; +import { ModalCloseBtn } from '@/components/common/modal/ModalWidgets'; +import CreateFrameForm, { + FrameFormValues, + defaultFrameFormValues, +} from './CreateFrameForm'; +import PostFrameForm from './PostFrameForm'; +import useLogin from '@/hooks/shared/useLogin'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; + +enum Steps { + CREATE_FRAME = 'CREATE_FRAME', + POST_FRAME = 'POST_FRAME', +} +const unpublishedFrameFormKey = 'red-envelope:unpublished-frame-form'; +const unpublishedFrameUrlKey = 'red-envelope:unpublished-frame-url'; +const getStoredUnpublishedFrame = () => { + let form = null; + let url = ''; + try { + const formJson = localStorage.getItem(unpublishedFrameFormKey); + if (formJson) { + form = JSON.parse(formJson); + } + } catch (error) { + /* empty */ + } + try { + url = localStorage.getItem(unpublishedFrameUrlKey) || ''; + } catch (error) { + /* empty */ + } + return { + form: form as FrameFormValues | null, + url, + }; +}; +const storeUnpublishedFrame = (form: any, url: string) => { + localStorage.setItem(unpublishedFrameFormKey, JSON.stringify(form)); + localStorage.setItem(unpublishedFrameUrlKey, url); +}; +const removeStoredUnpublishedFrame = () => { + localStorage.removeItem(unpublishedFrameFormKey); + localStorage.removeItem(unpublishedFrameUrlKey); +}; + +export default function CreateModal({ + open, + closeModal, +}: { + open: boolean; + closeModal: () => void; +}) { + const { isLogin, login } = useLogin(); + const { openFarcasterQR, isConnected: isLoginFarcaster } = useFarcasterCtx(); + + const [step, setStep] = useState(Steps.CREATE_FRAME); + const [frameFormValues, setFrameFormValues] = useState( + defaultFrameFormValues + ); + const [frameUrl, setFrameUrl] = useState(''); + useEffect(() => { + // validate unpublished frame + const { form, url } = getStoredUnpublishedFrame(); + if (url && form) { + setFrameFormValues(form); + setFrameUrl(url); + } + }, []); + + const submitFrame = (values: FrameFormValues) => { + if (!isLogin) { + login(); + return; + } + if (!isLoginFarcaster) { + openFarcasterQR(); + return; + } + + // TODO @ttang pledge degen + const { totalReward } = values; + + // TODO create frame + + // store frame + storeUnpublishedFrame(values, frameUrl); + setStep(Steps.POST_FRAME); + }; + + return ( + +
+
+ + Red Envelope + + +
+ {step === Steps.POST_FRAME ? ( + { + removeStoredUnpublishedFrame(); + }} + onBack={() => { + setStep(Steps.CREATE_FRAME); + }} + /> + ) : ( + <> + { + setFrameFormValues(values); + }} + onSubmit={(values) => { + submitFrame(values); + }} + /> + {!!frameUrl && !!frameFormValues && ( + + )} + + )} +
+
+ ); +} diff --git a/apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx b/apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx new file mode 100644 index 00000000..2c00a49c --- /dev/null +++ b/apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx @@ -0,0 +1,28 @@ +import { ComponentPropsWithRef } from 'react'; +import { FrameFormValues } from './CreateFrameForm'; + +type Props = ComponentPropsWithRef<'div'> & { + frame: FrameFormValues; +}; +export default function EmbedFramePreview({ frame, ...props }: Props) { + return ( +
+ +
+ +
+
+ ); +} diff --git a/apps/u3/src/components/social/frames/red-envelope/PostFrameForm.tsx b/apps/u3/src/components/social/frames/red-envelope/PostFrameForm.tsx new file mode 100644 index 00000000..6a426a10 --- /dev/null +++ b/apps/u3/src/components/social/frames/red-envelope/PostFrameForm.tsx @@ -0,0 +1,202 @@ +/* eslint-disable no-underscore-dangle */ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { CastAddBody, makeCastAdd } from '@farcaster/hub-web'; +import { toast } from 'react-toastify'; +import { Channelv1 } from '@mod-protocol/farcaster'; +import useLogin from '@/hooks/shared/useLogin'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { FARCASTER_NETWORK, FARCASTER_WEB_CLIENT } from '@/constants/farcaster'; +import FarcasterInput from '../../farcaster/FarcasterInput'; +import { FCastChannelPicker } from '../../farcaster/FCastChannelPicker'; +import { FrameFormValues } from './CreateFrameForm'; +import EmbedFramePreview from './EmbedFramePreview'; + +export type EmbedWebsiteLink = { + link: string; + showPreview?: boolean; + previewTitle?: string; + previewImg?: string; + previewDomain?: string; +}; +export default function PostFrameForm({ + frameUrl, + frameData, + channel, + defaultText, + onSuccess, + onBack, +}: { + frameUrl: string; + frameData: FrameFormValues; + channel?: Channelv1; + defaultText?: string; + onSuccess?: () => void; + onBack?: () => void; +}) { + const { isLogin: isLoginU3, login } = useLogin(); + const farcasterInputRef = useRef<{ + handleFarcasterSubmit: () => void; + }>(); + const [farcasterInputText, setFarcasterInputText] = useState(''); + + const { + encryptedSigner, + currFid: farcasterUserFid, + getChannelFromId, + } = useFarcasterCtx(); + const [channelValue, setChannelValue] = useState({ + name: 'Home', + parent_url: '', + image: 'https://warpcast.com/~/channel-images/home.png', + channel_id: 'home', + }); + const [isPending, setIsPending] = useState(false); + + const handleSubmitToFarcaster = useCallback( + async (castBody?: CastAddBody, c?: string[]) => { + if (!frameUrl) return; + if (!encryptedSigner) return; + // const currFid = getCurrFid(); + if (!farcasterUserFid) return; + const parentUrl = channelValue?.parent_url || undefined; + + const toChannels = (c || []) + .map((channelId) => { + return getChannelFromId(channelId); + }) + .filter((item) => item !== null); + + // console.log('castBody', castBody, toChannels, parentUrl); + try { + setIsPending(true); + const embedFrameLink = { url: frameUrl }; + const castBodySubmit = { + text: castBody?.text || farcasterInputText, + embeds: [...(castBody?.embeds || []), embedFrameLink], + embedsDeprecated: [], + mentions: castBody?.mentions || [], + mentionsPositions: castBody?.mentionsPositions || [], + parentUrl, + }; + console.log('castBodySubmit', castBodySubmit); + // eslint-disable-next-line no-underscore-dangle + if (toChannels.length === 0) { + const cast = ( + await makeCastAdd( + castBodySubmit, + { fid: farcasterUserFid, network: FARCASTER_NETWORK }, + encryptedSigner + ) + )._unsafeUnwrap(); + const result = await FARCASTER_WEB_CLIENT.submitMessage(cast); + if (result.isErr()) { + throw new Error(result.error.message); + } + } else { + const result = await Promise.all( + toChannels.map(async (toChannel) => { + const cast = { + ...castBodySubmit, + parentUrl: toChannel.parent_url, + }; + // console.log('cast', cast); + const r = await FARCASTER_WEB_CLIENT.submitMessage( + ( + await makeCastAdd( + cast, + { fid: farcasterUserFid, network: FARCASTER_NETWORK }, + encryptedSigner + ) + )._unsafeUnwrap() + ); + return r; + }) + ); + result.forEach((r) => { + if (r.isErr()) { + throw new Error(r.error.message); + } + }); + } + + // cleanImage(); + if (onSuccess) onSuccess(); + toast.success('successfully posted to farcaster'); + } catch (error: unknown) { + console.error(error); + toast.error('failed to post to farcaster'); + } finally { + setIsPending(false); + } + }, + [ + farcasterInputText, + encryptedSigner, + channel, + channelValue, + farcasterUserFid, + frameUrl, + getChannelFromId, + ] + ); + + useEffect(() => { + if (channel) { + setChannelValue(channel); + } + }, [channel]); + + useEffect(() => { + setFarcasterInputText(defaultText || ''); + }, [defaultText]); + + return ( +
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx b/apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx new file mode 100644 index 00000000..142a7f31 --- /dev/null +++ b/apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx @@ -0,0 +1,45 @@ +import { ComponentPropsWithRef, useState } from 'react'; +import { cn } from '@/lib/utils'; +import CreateModal from './CreateModal'; + +export default function RedEnvelopeFloatingWindow() { + const [open, setOpen] = useState(false); + return ( +
+
🧧 Red Envelope
+ { + setOpen(true); + }} + > + Create My Red Envelope 🧧 + + setOpen(false)} /> +
+ ); +} + +function CommonButton({ + className, + ...props +}: ComponentPropsWithRef<'button'>) { + return ( + diff --git a/apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx b/apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx index f351de5d..60666dbe 100644 --- a/apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx +++ b/apps/u3/src/components/social/frames/red-envelope/CreateModal.tsx @@ -12,51 +12,52 @@ import { useAccount, useNetwork } from 'wagmi'; import ModalContainer from '@/components/common/modal/ModalContainer'; import { ModalCloseBtn } from '@/components/common/modal/ModalWidgets'; -import CreateFrameForm, { - FrameFormValues, - defaultFrameFormValues, -} from './CreateFrameForm'; +import CreateFrameForm, { defaultFrameFormValues } from './CreateFrameForm'; import PostFrameForm from './PostFrameForm'; import useLogin from '@/hooks/shared/useLogin'; import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; import { DegenABI, DegenAddress } from '@/services/social/abi/degen/contract'; +import { + CreateRedEnvelopeParams, + createRedEnvelope, +} from '@/services/frames/api/red-envelope'; +import { API_BASE_URL, RED_ENVELOPE_PLEDGE_ADDRESS } from '@/constants'; +import { RedEnvelopeEntity } from '@/services/frames/types/red-envelope'; +import ModalBase, { ModalBaseBody } from '@/components/common/modal/ModalBase'; -const U3Address = '0xCFd3527F4334Ebb2E3b53b01f70B7BD5C3170cD5'; +const RED_ENVELOPE_FRAME_HOST = API_BASE_URL; enum Steps { CREATE_FRAME = 'CREATE_FRAME', POST_FRAME = 'POST_FRAME', } -const unpublishedFrameFormKey = 'red-envelope:unpublished-frame-form'; -const unpublishedFrameUrlKey = 'red-envelope:unpublished-frame-url'; -const getStoredUnpublishedFrame = () => { - let form = null; - let url = ''; +const unpublishedRedEnvelopeFrameDataKey = + 'red-envelope:unpublished-frame-data'; +const getStoredUnpublishedData = () => { + let data = null; try { - const formJson = localStorage.getItem(unpublishedFrameFormKey); - if (formJson) { - form = JSON.parse(formJson); + const dataJson = localStorage.getItem(unpublishedRedEnvelopeFrameDataKey); + if (dataJson) { + data = JSON.parse(dataJson); } } catch (error) { /* empty */ } - try { - url = localStorage.getItem(unpublishedFrameUrlKey) || ''; - } catch (error) { - /* empty */ - } return { - form: form as FrameFormValues | null, - url, + data: data as RedEnvelopeEntity, }; }; -const storeUnpublishedFrame = (form: any, url: string) => { - localStorage.setItem(unpublishedFrameFormKey, JSON.stringify(form)); - localStorage.setItem(unpublishedFrameUrlKey, url); +const storeUnpublishedFrameData = (data: RedEnvelopeEntity) => { + localStorage.setItem( + unpublishedRedEnvelopeFrameDataKey, + JSON.stringify(data) + ); +}; +const removeStoredUnpublishedFrameData = () => { + localStorage.removeItem(unpublishedRedEnvelopeFrameDataKey); }; -const removeStoredUnpublishedFrame = () => { - localStorage.removeItem(unpublishedFrameFormKey); - localStorage.removeItem(unpublishedFrameUrlKey); +const getFrameUrl = (data: RedEnvelopeEntity) => { + return `${RED_ENVELOPE_FRAME_HOST}/frames/red-envelope/${data.id}`; }; export default function CreateModal({ @@ -72,15 +73,20 @@ export default function CreateModal({ const { openFarcasterQR, isConnected: isLoginFarcaster } = useFarcasterCtx(); const [step, setStep] = useState(Steps.CREATE_FRAME); + const [submitting, setSubmitting] = useState(false); const [frameFormValues, setFrameFormValues] = useState( defaultFrameFormValues ); + const [createdFrameData, setCreatedFrameData] = + useState(null); const [frameUrl, setFrameUrl] = useState(''); useEffect(() => { // validate unpublished frame - const { form, url } = getStoredUnpublishedFrame(); - if (url && form) { - setFrameFormValues(form); + const { data } = getStoredUnpublishedData(); + if (data) { + setFrameFormValues(data); + setCreatedFrameData(data); + const url = getFrameUrl(data); setFrameUrl(url); } }, []); @@ -88,6 +94,9 @@ export default function CreateModal({ const pledgeDegenToU3 = useCallback( async (amount: string | number) => { try { + if (!RED_ENVELOPE_PLEDGE_ADDRESS) { + throw new Error('RED_ENVELOPE_PLEDGE_ADDRESS is not defined'); + } if (network.chain?.id !== base.id) { await switchNetwork({ chainId: base.id }); } @@ -96,7 +105,7 @@ export default function CreateModal({ abi: DegenABI, chainId: base.id, functionName: 'transfer', - args: [U3Address, parseEther(amount.toString())], + args: [RED_ENVELOPE_PLEDGE_ADDRESS, parseEther(amount.toString())], }); const degenTxHash = await writeContract(transferDegenRequest); const degenTxReceipt = await waitForTransaction({ @@ -118,9 +127,8 @@ export default function CreateModal({ }, [network] ); - const submitFrame = useCallback( - async (values: FrameFormValues) => { + async (values: CreateRedEnvelopeParams) => { if (!isLogin || !accountAddr) { login(); return; @@ -130,78 +138,91 @@ export default function CreateModal({ return; } - // pledge degen - const { totalReward } = values; - const txHash = await pledgeDegenToU3(totalReward); - if (!txHash) { - toast.error('pledge degen failed'); - return; + try { + setSubmitting(true); + // pledge degen + const { totalAmount } = values; + const txHash = await pledgeDegenToU3(totalAmount); + if (!txHash) { + toast.error('pledge degen failed'); + return; + } + console.log(txHash, totalAmount); + const res = await createRedEnvelope({ ...values, txHash }); + const data = res?.data?.data; + if (data.id) { + const url = getFrameUrl(data); + setFrameUrl(url); + setCreatedFrameData(data); + storeUnpublishedFrameData(data); + setStep(Steps.POST_FRAME); + toast.success('Red envelope created'); + } + } catch (error) { + toast.error(error.message); + } finally { + setSubmitting(false); } - console.log(txHash, totalReward); - - // TODO create frame - - // store frame - storeUnpublishedFrame(values, frameUrl); - setStep(Steps.POST_FRAME); }, - [isLogin, isLoginFarcaster, openFarcasterQR, frameUrl, accountAddr] + [isLogin, isLoginFarcaster, openFarcasterQR, accountAddr] ); return ( - -
-
- - Red Envelope - - -
- {step === Steps.POST_FRAME ? ( - { - removeStoredUnpublishedFrame(); - }} - onBack={() => { - setStep(Steps.CREATE_FRAME); - }} - /> - ) : ( - <> - { - setFrameFormValues(values); + +
+
+
+ + Red Envelope + + +
+ {step === Steps.POST_FRAME ? ( + { + setStep(Steps.CREATE_FRAME); + removeStoredUnpublishedFrameData(); + setFrameFormValues(defaultFrameFormValues); + setCreatedFrameData(null); + setFrameUrl(''); }} - onSubmit={(values) => { - submitFrame(values); + onBack={() => { + setStep(Steps.CREATE_FRAME); }} /> - {!!frameUrl && !!frameFormValues && ( - - )} - - )} + onClick={() => { + setStep(Steps.POST_FRAME); + }} + > + The red envelope is ready, go publish it. + + )} + + )} +
- +
); } diff --git a/apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx b/apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx index 2c00a49c..99a576cd 100644 --- a/apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx +++ b/apps/u3/src/components/social/frames/red-envelope/EmbedFramePreview.tsx @@ -1,20 +1,119 @@ -import { ComponentPropsWithRef } from 'react'; -import { FrameFormValues } from './CreateFrameForm'; +import { ComponentPropsWithRef, useEffect, useState } from 'react'; +import satori from 'satori'; +import Loading from '@/components/common/loading/Loading'; +import { RedEnvelopeEntity } from '@/services/frames/types/red-envelope'; +import { getUserinfoWithFid } from '@/services/social/api/farcaster'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { getBase64FromSvg } from '@/utils/shared/getBase64FromUrl'; type Props = ComponentPropsWithRef<'div'> & { - frame: FrameFormValues; + frameData: RedEnvelopeEntity; }; -export default function EmbedFramePreview({ frame, ...props }: Props) { +export default function EmbedFramePreview({ frameData, ...props }: Props) { + const [generating, setGenerating] = useState(false); + const [frameImg, setFrameImg] = useState(''); + const { currFid } = useFarcasterCtx(); + + useEffect(() => { + (async () => { + try { + setGenerating(true); + let creatorInfo = { fname: '' }; + try { + const creatorFid = frameData?.creatorFid || ''; + console.log('frameData', frameData); + console.log('currFid', currFid); + + const { data } = await getUserinfoWithFid( + creatorFid || String(currFid) || '' + ); + creatorInfo = data.data; + } catch (error) { + console.error(error); + } + + const host = window.location.origin; + + const fontDataMontserrat700 = await fetch( + new URL(`fonts/red-envelope/montserrat/Montserrat-Bold.otf`, host) + ).then((res) => res.arrayBuffer()); + + const fontDataMontserrat500 = await fetch( + new URL(`fonts/red-envelope/montserrat/Montserrat-Medium.otf`, host) + ).then((res) => res.arrayBuffer()); + + const fontDataInter = await fetch( + new URL(`fonts/red-envelope/inter/Inter-Bold.ttf`, host) + ).then((res) => res.arrayBuffer()); + + const fontDataEricaOne = await fetch( + new URL(`fonts/red-envelope/erica-one/EricaOne-Regular.ttf`, host) + ).then((res) => res.arrayBuffer()); + + const svg = await satori( + , + { + width: 800, + height: 418, + fonts: [ + { + name: 'Montserrat', + // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here. + data: fontDataMontserrat700, + weight: 700, + style: 'normal', + }, + { + name: 'Montserrat', + // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here. + data: fontDataMontserrat500, + weight: 500, + style: 'normal', + }, + { + name: 'Inter', + // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here. + data: fontDataInter, + weight: 700, + style: 'normal', + }, + { + name: 'Erica One', + // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here. + data: fontDataEricaOne, + weight: 400, + style: 'normal', + }, + ], + } + ); + // svg to base64 + const base64Img = await getBase64FromSvg(svg); + setFrameImg(base64Img as string); + } catch (error) { + console.error(error); + } finally { + setGenerating(false); + } + })(); + }, [frameData, currFid]); return (
- +
+ {generating ? ( + + ) : ( + + )} + {/* */} +
+
+
+
); }