Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Merge Accounts
Merge accounts dialog with account selection, data preview, conflict resolution, and confirmation
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Combine duplicate accounts safely. This React merge accounts dialog provides source and target account selection, data preview showing what will be merged, conflict resolution options for duplicate fields, and confirmation step before executing. Built with shadcn/ui Dialog, RadioGroup, Alert, Avatar, Button, and Badge components using Tailwind CSS, users consolidate accounts confidently. Select accounts, review data, resolve conflicts—perfect for CRM systems, user management, customer databases, or any Next.js application requiring account deduplication with data integrity protection.
"use client";import { useState } from "react";import { Merge, Check, Loader2, AlertTriangle, ArrowRight, User, Mail, Phone, Calendar, Activity,} from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";import { Label } from "@/components/ui/label";import { Badge } from "@/components/ui/badge";import { Alert, AlertDescription } from "@/components/ui/alert";import { Separator } from "@/components/ui/separator";import { cn } from "@/lib/utils";type Account = { id: string; name: string; email: string; phone: string; avatar?: string; initials: string; createdAt: string; lastActive: string; activityCount: number;};const accounts: Account[] = [ { id: "1", name: "John Smith", email: "[email protected]", phone: "+1 (555) 123-4567", initials: "JS", createdAt: "Jan 15, 2023", lastActive: "2 hours ago", activityCount: 156, }, { id: "2", name: "John D. Smith", email: "[email protected]", phone: "+1 (555) 987-6543", initials: "JS", createdAt: "Mar 22, 2024", lastActive: "5 days ago", activityCount: 23, },];type ConflictField = "email" | "phone";export const title = "React Dialog Block Merge Accounts";export default function DialogMergeAccounts() { const [open, setOpen] = useState(false); const [primaryAccount, setPrimaryAccount] = useState<string>(accounts[0].id); const [conflicts, setConflicts] = useState<Record<ConflictField, string>>({ email: accounts[0].id, phone: accounts[0].id, }); const [isMerging, setIsMerging] = useState(false); const [step, setStep] = useState<"select" | "review">("select"); const primary = accounts.find((a) => a.id === primaryAccount)!; const secondary = accounts.find((a) => a.id !== primaryAccount)!; const handleMerge = async () => { setIsMerging(true); await new Promise((resolve) => setTimeout(resolve, 1500)); setIsMerging(false); setOpen(false); setStep("select"); }; const handleConflictChange = (field: ConflictField, value: string) => { setConflicts((prev) => ({ ...prev, [field]: value })); }; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Merge className="mr-2 h-4 w-4" /> Merge Accounts </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <Merge className="h-5 w-5" /> Merge Accounts </DialogTitle> <DialogDescription> {step === "select" ? "Select the primary account to keep." : "Review and resolve any conflicts."} </DialogDescription> </DialogHeader> {step === "select" ? ( <div className="space-y-4 py-4"> <Alert> <AlertTriangle className="h-4 w-4" /> <AlertDescription> The secondary account will be merged into the primary account. This action combines all data and cannot be easily undone. </AlertDescription> </Alert> <RadioGroup value={primaryAccount} onValueChange={setPrimaryAccount} className="space-y-3" > {accounts.map((account) => ( <Label key={account.id} htmlFor={account.id} className={cn( "flex items-start gap-4 rounded-lg border p-4 cursor-pointer transition-all", primaryAccount === account.id ? "border-primary bg-primary/5" : "hover:border-muted-foreground/50" )} > <RadioGroupItem value={account.id} id={account.id} className="mt-1" /> <Avatar className="h-10 w-10"> <AvatarImage src={account.avatar} /> <AvatarFallback>{account.initials}</AvatarFallback> </Avatar> <div className="flex-1 space-y-1"> <div className="flex items-center gap-2"> <span className="font-medium">{account.name}</span> {primaryAccount === account.id && ( <Badge variant="default" className="text-xs">Primary</Badge> )} </div> <p className="text-sm text-muted-foreground">{account.email}</p> <div className="flex items-center gap-4 text-xs text-muted-foreground"> <span className="flex items-center gap-1"> <Calendar className="h-3 w-3" /> Created {account.createdAt} </span> <span className="flex items-center gap-1"> <Activity className="h-3 w-3" /> {account.activityCount} activities </span> </div> </div> </Label> ))} </RadioGroup> </div> ) : ( <div className="space-y-4 py-4"> {/* Merge Direction */} <div className="flex items-center justify-center gap-4 py-2"> <div className="text-center"> <Avatar className="h-12 w-12 mx-auto"> <AvatarFallback>{secondary.initials}</AvatarFallback> </Avatar> <p className="text-xs text-muted-foreground mt-1">Secondary</p> </div> <ArrowRight className="h-5 w-5 text-muted-foreground" /> <div className="text-center"> <Avatar className="h-12 w-12 mx-auto border-2 border-primary"> <AvatarFallback>{primary.initials}</AvatarFallback> </Avatar> <p className="text-xs font-medium mt-1">Primary</p> </div> </div> <Separator /> {/* Conflict Resolution */} <div className="space-y-4"> <Label className="text-sm font-medium">Resolve Conflicts</Label> {/* Email Conflict */} <div className="space-y-2"> <div className="flex items-center gap-2 text-sm"> <Mail className="h-4 w-4 text-muted-foreground" /> <span>Email Address</span> </div> <RadioGroup value={conflicts.email} onValueChange={(v) => handleConflictChange("email", v)} className="grid grid-cols-2 gap-2" > {accounts.map((account) => ( <Label key={account.id} htmlFor={`email-${account.id}`} className={cn( "flex items-center gap-2 rounded-md border p-2 cursor-pointer text-sm", conflicts.email === account.id && "border-primary bg-primary/5" )} > <RadioGroupItem value={account.id} id={`email-${account.id}`} /> <span className="truncate">{account.email}</span> </Label> ))} </RadioGroup> </div> {/* Phone Conflict */} <div className="space-y-2"> <div className="flex items-center gap-2 text-sm"> <Phone className="h-4 w-4 text-muted-foreground" /> <span>Phone Number</span> </div> <RadioGroup value={conflicts.phone} onValueChange={(v) => handleConflictChange("phone", v)} className="grid grid-cols-2 gap-2" > {accounts.map((account) => ( <Label key={account.id} htmlFor={`phone-${account.id}`} className={cn( "flex items-center gap-2 rounded-md border p-2 cursor-pointer text-sm", conflicts.phone === account.id && "border-primary bg-primary/5" )} > <RadioGroupItem value={account.id} id={`phone-${account.id}`} /> <span className="truncate">{account.phone}</span> </Label> ))} </RadioGroup> </div> </div> <Separator /> {/* Summary */} <div className="rounded-lg border p-3 space-y-2"> <p className="text-sm font-medium">After merge:</p> <ul className="text-sm text-muted-foreground space-y-1"> <li>• Name: {primary.name}</li> <li>• Email: {accounts.find((a) => a.id === conflicts.email)?.email}</li> <li>• Phone: {accounts.find((a) => a.id === conflicts.phone)?.phone}</li> <li>• Activities: {accounts.reduce((sum, a) => sum + a.activityCount, 0)} total</li> </ul> </div> </div> )} <DialogFooter> {step === "select" ? ( <> <Button variant="outline" onClick={() => setOpen(false)}> Cancel </Button> <Button onClick={() => setStep("review")}> Continue <ArrowRight className="ml-2 h-4 w-4" /> </Button> </> ) : ( <> <Button variant="outline" onClick={() => setStep("select")} disabled={isMerging} > Back </Button> <Button onClick={handleMerge} disabled={isMerging}> {isMerging ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Merging... </> ) : ( <> <Check className="mr-2 h-4 w-4" /> Merge Accounts </> )} </Button> </> )} </DialogFooter> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-merge-accounts.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-merge-accounts.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-merge-accounts.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-merge-accounts.jsonRelated blocks you will also like
React Dialog Block Connect Account
Account linking
React Dialog Block Delete Account
Account removal
React Dialog Block Confirm Action
Confirmation flow
React Dialog Block Data Import
Data management
React Dialog Block Bulk Actions
Batch operations
React Dialog Block Edit Profile
Profile management