shadimage
lens-blocks
blockcollect-dialogsource: ok · hit

Lens Collect Post Dialog

A dialog component for collecting Lens Posts with support for paid collects.

npx shadcn add "https://lensblocks.com/r/collect-dialog.json"
free

Interaction

modal

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Registry dependencies

buttondialoghttps://lensblocks.com/r/post-provider.jsonhttps://lensblocks.com/r/use-post-context.jsonhttps://lensblocks.com/r/utils.json

Files1 · source cached 10/8/2026, 5:27:24 PM

registry/new-york/blocks/feed/lens-collect-dialog.tsx
"use client";

import { PaymentSource, TxHash, useAuthenticatedUser } from "@lens-protocol/react";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { BoxIcon, CircleDollarSign, Clock, Snowflake, Users } from "lucide-react";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/registry/new-york/ui/dialog";
import moment from "moment/moment";
import { Button } from "@/registry/new-york/ui/button";
import { Referral } from "@/registry/new-york/lib/lens-post-provider";
import { useLensPostContext } from "@/registry/new-york/hooks/use-lens-post-context";
import { getUsernamePath, truncateAddress } from "@/registry/new-york/lib/lens-utils";

export type CollectDialogRef = {
  open: () => void;
  close: () => void;
  isOpen: boolean;
};

type CollectDialogProps = {
  referrals?: Referral[];
  onSuccess?: (txHash: TxHash) => void;
  onError?: (error: Error) => void;
  nftUrlPattern?: string;
};

export const LensCollectDialog = forwardRef<CollectDialogRef, CollectDialogProps>((props, ref) => {
  const { referrals, onSuccess, onError, nftUrlPattern = "https://lenscan.io/nfts/{collectNftAddress}" } = props;
  const { post, collect } = useLensPostContext();

  const [collectDialogOpen, setCollectDialogOpen] = useState(false);
  const [collecting, setCollecting] = useState(false);
  const [collectEnded, setCollectEnded] = useState(false);
  const [collectAvailable, setCollectAvailable] = useState(false);

  const { data: user } = useAuthenticatedUser();

  const action = post && "actions" in post && post.actions?.find(action => action.__typename === "SimpleCollectAction");
  const operations = post && "operations" in post ? post.operations : null;
  const stats = post && "stats" in post ? post.stats : null;

  useImperativeHandle(ref, () => ({
    open: () => setCollectDialogOpen(true),
    close: () => setCollectDialogOpen(false),
    isOpen: collectDialogOpen,
  }));

  useEffect(() => {
    if (!action) {
      setCollectAvailable(false);
      onError?.(new Error("This post is not collectable"));
      return;
    }

    if (action.endsAt) {
      setCollectEnded(moment().isAfter(action.endsAt));
    }

    if (!operations?.canSimpleCollect || operations?.hasSimpleCollected) {
      setCollectAvailable(false);
      return;
    }

    if (action.collectLimit) {
      setCollectAvailable((stats?.collects ?? 0) < action.collectLimit);
    }
  }, [action]);

  const onCollectClick = async () => {
    setCollecting(true);
    try {
      const txHash = await collect(PaymentSource.Signer, referrals);
      if (txHash) {
        setCollectDialogOpen(false);
        onSuccess?.(txHash);
      }
    } catch (e: any) {
      if (e instanceof Error) {
        onError?.(e);
      }
    } finally {
      setCollecting(false);
    }
  };

  if (!action) {
    return null;
  }

  return (
    <Dialog open={collectDialogOpen} onOpenChange={setCollectDialogOpen}>
      <DialogContent className="sm:max-w-sm">
        <DialogHeader className="border-b">
          <DialogTitle>Collect</DialogTitle>
        </DialogHeader>
        <div className="flex flex-col gap-1 px-4">
          <div className="text-lg font-bold pb-3">
            {"collectibleMetadata" in post && post.collectibleMetadata.name ? (
              post.collectibleMetadata.name
            ) : (
              <span>
                Post by{" "}
                {post.author.username ? (
                  <a
                    href={getUsernamePath(post.author.username.value)}
                    className="!font-bold"
                    target="_blank"
                    rel="noopener noreferrer"
                  >
                    @{post.author.username.localName}
                  </a>
                ) : (
                  <span className="!font-bold">{truncateAddress(post.author.address)}</span>
                )}
              </span>
            )}
          </div>
          {action.payToCollect && (
            <div className="flex gap-2.5 items-center">
              <CircleDollarSign className="w-7 h-7" />
              <span className="font-bold">
                <span className="text-xl">{action.payToCollect.amount.value}</span>{" "}
                {action.payToCollect.amount.asset.symbol}
              </span>
            </div>
          )}
          <div className="flex gap-4 items-center pl-1.5">
            <BoxIcon className="w-4 h-4 opacity-60" />
            <a
              href={nftUrlPattern.replace("{collectNftAddress}", action.collectNftAddress)}
              className="font-semibold hover:underline"
              target="_blank"
              rel="noopener noreferrer"
            >
              {truncateAddress(action.collectNftAddress, 10)}
            </a>
          </div>
          <div className="flex gap-4 items-center pl-1.5">
            <Users className="w-4 h-4 opacity-60" />
            <span>
              <span className="font-semibold">{new Intl.NumberFormat().format(stats?.collects ?? 0)}</span>{" "}
              {stats?.collects === 1 ? "collector" : "collectors"}
            </span>
          </div>
          {action.collectLimit && (
            <div className="flex gap-4 items-center pl-1.5">
              <Snowflake className="w-4 h-4 opacity-60" />
              <span>
                <span className="font-semibold">
                  {new Intl.NumberFormat().format(action.collectLimit - (stats?.collects ?? 0))} of{" "}
                  {action.collectLimit}
                </span>{" "}
                remaining
              </span>
            </div>
          )}
          {action.endsAt && (
            <div className="flex gap-4 items-center pl-1.5">
              <Clock className="w-4 h-4 opacity-60" />
              <abbr className="!border-b-0 no-underline" title={moment(action.endsAt).format("LLL")}>
                {collectEnded ? "Ended " : "Ends "}{" "}
                <span className="font-semibold">{moment(action.endsAt).fromNow()}</span>
              </abbr>
            </div>
          )}
        </div>
        <DialogFooter className="flex justify-end pt-2">
          <Button
            size="lg"
            className="font-bold"
            onClick={onCollectClick}
            disabled={collecting || collectEnded || !collectAvailable}
          >
            {collecting ? (
              <span className="loader"></span>
            ) : !user?.address ? (
              "Log in to collect"
            ) : operations?.hasSimpleCollected ? (
              "Collected"
            ) : collectEnded ? (
              "No longer available"
            ) : collectAvailable ? (
              action.payToCollect ? (
                `Collect for ${action.payToCollect.amount.value} ${action.payToCollect.amount.asset.symbol}`
              ) : (
                "Collect for free"
              )
            ) : (
              "Unable to collect"
            )}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
});
export default LensCollectDialog;

More from lens-blocks

Browse all components in this library →