Integrate Mod Editor

Getting started with React

See a live example (opens in a new tab)

Add the core libraries

yarn add @mod-protocol/react @mod-protocol/react-editor @mod-protocol/miniapp-registry

Add the default UI library, or use your own. The default uses shadcn (opens in a new tab) which uses Tailwind (opens in a new tab) + Radix UI (opens in a new tab) under the hood We've made these packages modular to reduce unnecessary packages, facilitate tree shaking, and allow for flexibility to integrate natively with your UI.

yarn add @mod-protocol/react-ui-shadcn

Mod Editor for Farcaster Cast creation

import * as React from "react";
 
// Core
import {
  Channel,
  getFarcasterChannels,
  getFarcasterMentions,
} from "@mod-protocol/farcaster";
import { CreationMiniApp } from "@mod-protocol/react";
import { useEditor, EditorContent } from "@mod-protocol/react-editor";
import { creationMiniApps } from "@mod-protocol/miniapp-registry";
import {
  Embed,
  ModManifest,
  fetchUrlMetadata,
  handleAddEmbed,
  handleOpenFile,
  handleSetInput,
} from "@mod-protocol/core";
 
// UI implementation
import { createRenderMentionsSuggestionConfig } from "@mod-protocol/react-ui-shadcn/dist/lib/mentions";
import { CreationMiniAppsSearch } from "@mod-protocol/react-ui-shadcn/dist/components/creation-miniapps-search";
import { CastLengthUIIndicator } from "@mod-protocol/react-ui-shadcn/dist/components/cast-length-ui-indicator";
import { ChannelPicker } from "@mod-protocol/react-ui-shadcn/dist/components/channel-picker";
import { EmbedsEditor } from "@mod-protocol/react-ui-shadcn/dist/lib/embeds";
import { Button } from "@mod-protocol/react-ui-shadcn/dist/components/ui/button";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@mod-protocol/react-ui-shadcn/dist/components/ui/popover";
import { renderers } from "@mod-protocol/react-ui-shadcn/dist/renderers";
 
// Optionally replace with your API_URL here if you want to self host by running your own instance of https://github.com/mod-protocol/mod/tree/main/examples/api
const API_URL = "https://api.modprotocol.org";
const getResults = getFarcasterMentions(API_URL);
const getChannels = getFarcasterChannels(API_URL);
const getUrlMetadata = fetchUrlMetadata(API_URL);
const onError = (err) => window.alert(err.message);
const onSubmit = async ({
  text,
  embeds,
  channel,
}: {
  text: string;
  embeds: Embed[];
  channel: Channel;
}) => {
  window.alert(
    `This is a demo, and doesn't do anything.\n\nCast text:\n${text}\nEmbeds:\n${embeds
      .map((embed) => (embed as any).url)
      .join(", ")}\nChannel:\n${channel.name}`
  );
 
  return true;
};
 
export default function EditorExample() {
  const {
    editor,
    getText,
    getEmbeds,
    setEmbeds,
    setText,
    setChannel,
    getChannel,
    addEmbed,
    handleSubmit,
  } = useEditor({
    fetchUrlMetadata: getUrlMetadata,
    onError,
    onSubmit,
    linkClassName: "text-blue-600",
    renderMentionsSuggestionConfig: createRenderMentionsSuggestionConfig({
      getResults: getResults,
    }),
  });
 
  const [currentMiniapp, setCurrentMiniapp] =
    React.useState<ModManifest | null>(null);
 
  return (
    <form onSubmit={handleSubmit}>
      <div className="p-2 border-slate-200 rounded-md border">
        <EditorContent
          editor={editor}
          autoFocus
          className="w-full h-full min-h-[200px]"
        />
        <EmbedsEditor embeds={getEmbeds()} setEmbeds={setEmbeds} />
      </div>
      <div className="flex flex-row pt-2 gap-1">
        <ChannelPicker
          getChannels={getChannels}
          onSelect={setChannel}
          value={getChannel()}
        />
        <Popover
          open={!!currentMiniapp}
          onOpenChange={(op: boolean) => {
            if (!op) setCurrentMiniapp(null);
          }}
        >
          <PopoverTrigger></PopoverTrigger>
          <CreationMiniAppsSearch
            miniapps={creationMiniApps}
            onSelect={setCurrentMiniapp}
          />
          <PopoverContent className="w-[400px] ml-2" align="start">
            <div className="space-y-4">
              <h4 className="font-medium leading-none">
                {currentMiniapp?.name}
              </h4>
              <hr />
              <CreationMiniApp
                input={getText()}
                embeds={getEmbeds()}
                api={API_URL}
                variant="creation"
                manifest={currentMiniapp}
                renderers={renderers}
                onOpenFileAction={handleOpenFile}
                onExitAction={() => setCurrentMiniapp(null)}
                onSetInputAction={handleSetInput(setText)}
                onAddEmbedAction={handleAddEmbed(addEmbed)}
              />
            </div>
          </PopoverContent>
        </Popover>
 
        <CastLengthUIIndicator getText={getText} />
        <div className="grow"></div>
        <Button type="submit">Cast</Button>
      </div>
    </form>
  );
}

Embed rendering Mini-apps

"use client";
 
import { ContextType, Embed } from "@mod-protocol/core";
import {
  contentMiniApps,
  defaultContentMiniApp,
} from "@mod-protocol/miniapp-registry";
import { RenderEmbed } from "@mod-protocol/react";
import { renderers } from "@mod-protocol/react-ui-shadcn/dist/renderers";
import {
  sendTransaction,
  switchNetwork,
  waitForTransaction,
} from "@wagmi/core";
import { useMemo } from "react";
import { useAccount } from "wagmi";
 
export function Embeds(props: { embeds: Array<Embed> }) {
  const { address } = useAccount();
 
  // Handle send transaction e.g. on click of a mint button
  // This example uses wagmi
  const onSendEthTransactionAction = useMemo(
    () =>
      async ({ data, chainId }, { onConfirmed, onError, onSubmitted }) => {
        try {
          const parsedChainId = parseInt(chainId);
 
          // Switch chains if the user is not on the right one
          await switchNetwork({ chainId: parsedChainId });
 
          // Send the transaction
          const { hash } = await sendTransaction({
            ...data,
            chainId: parsedChainId,
          });
          onSubmitted(hash);
 
          // Wait for the transaction to be confirmed
          const { status } = await waitForTransaction({
            hash,
            chainId: parsedChainId,
          });
 
          onConfirmed(hash, status === "success");
        } catch (e) {
          onError(e);
        }
      },
    []
  );
 
  const context = useMemo<Omit<ContextType, "embed">>(() => {
    return {
      /* Required context */
      api: process.env.NEXT_PUBLIC_API_URL,
 
      /* Optional context */
      user: {
        id: "3",                // Current user's FID
        wallet: {
          address: "0x1234...", // Current user's wallet address
        }
      },
    };
  }, [address]);
 
  return (
    <div>
      {props.embeds.map((embed, i) => (
        <RenderEmbed
          embed={embed}
          {...context}
          key={i}
          renderers={renderers}
          defaultContentMiniApp={defaultContentMiniApp}
          contentMiniApps={contentMiniApps}
          resolvers={{
            onSendEthTransactionAction,
          }}
        />
      ))}
    </div>
  );
}

Integrating Creation Mini-apps with your own editor

You can integrate Creation Mini-apps with an existing editor, just pass the text and embeds from your editor into the CreationMiniApp component.