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 · lighticonslucide-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;