diff --git a/src/app/exicon/[entryId]/page.tsx b/src/app/exicon/[entryId]/page.tsx index fdbf284..67dc167 100644 --- a/src/app/exicon/[entryId]/page.tsx +++ b/src/app/exicon/[entryId]/page.tsx @@ -16,7 +16,7 @@ import Link from "next/link"; import { getEntryByIdFromDatabase } from "@/lib/api"; import CopyLinkButton from "@/components/shared/CopyLinkButton"; import { SuggestEditsButton } from "@/components/shared/SuggestEditsButton"; -import { CopyEntryUrlButton } from "@/components/shared/CopyEntryUrlButton"; +import { CopyEntryButton } from "@/components/shared/CopyEntryButton"; import { BackButton } from "@/components/shared/BackButton"; import { RichTextDisplay } from "@/components/shared/RichTextDisplay"; import { isHtmlContent } from "@/lib/sanitizeHtml"; @@ -200,7 +200,12 @@ export default async function ExiconEntryPage({ )}
- +
diff --git a/src/app/lexicon/[entryId]/page.tsx b/src/app/lexicon/[entryId]/page.tsx index e83ca7a..0c91d4a 100644 --- a/src/app/lexicon/[entryId]/page.tsx +++ b/src/app/lexicon/[entryId]/page.tsx @@ -10,7 +10,7 @@ import type { LexiconEntry } from "@/lib/types"; import type { Metadata } from "next"; import { getEntryByIdFromDatabase } from "@/lib/api"; import { SuggestEditsButton } from "@/components/shared/SuggestEditsButton"; -import { CopyEntryUrlButton } from "@/components/shared/CopyEntryUrlButton"; +import { CopyEntryButton } from "@/components/shared/CopyEntryButton"; import { BackButton } from "@/components/shared/BackButton"; import { RichTextDisplay } from "@/components/shared/RichTextDisplay"; import { isHtmlContent } from "@/lib/sanitizeHtml"; @@ -137,7 +137,12 @@ export default async function LexiconEntryPage({
- +
diff --git a/src/components/shared/CopyEntryButton.tsx b/src/components/shared/CopyEntryButton.tsx new file mode 100644 index 0000000..f62116f --- /dev/null +++ b/src/components/shared/CopyEntryButton.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useState } from "react"; +import { Copy, Check, ChevronDown } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useToast } from "@/hooks/use-toast"; +import { + copyToClipboard, + isInIframe as isInIframeUtil, + showCopyPrompt, +} from "@/lib/clipboard"; +import { generateEntryUrl } from "@/lib/route-utils"; +import type { AnyEntry } from "@/lib/types"; + +interface CopyEntryButtonProps { + entry: AnyEntry; + variant?: "default" | "ghost" | "outline"; + size?: "default" | "sm" | "lg" | "icon"; + showLabel?: boolean; + className?: string; +} + +export function CopyEntryButton({ + entry, + variant = "ghost", + size = "icon", + showLabel = false, + className = "", +}: CopyEntryButtonProps) { + const { toast } = useToast(); + const [copied, setCopied] = useState(false); + + const stripHtml = (html: string) => { + return html.replace(/<[^>]*>/g, "").replace(/ /g, " ").trim(); + }; + + const handleCopyUrl = async (event?: React.MouseEvent) => { + event?.stopPropagation(); + const url = generateEntryUrl(entry.id, entry.type as "exicon" | "lexicon"); + + const result = await copyToClipboard(url); + + if (result.success) { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + toast({ + title: `${entry.name} URL Copied!`, + }); + } else { + if (isInIframeUtil()) { + showCopyPrompt(url); + toast({ + title: "Manual Copy Required", + description: "Please copy the link from the popup dialog.", + }); + } else { + toast({ + title: "Failed to Copy URL", + description: result.error || "Could not copy the entry URL.", + variant: "destructive", + }); + } + } + }; + + const handleCopyDetails = async (event?: React.MouseEvent) => { + event?.stopPropagation(); + + const cleanDescription = entry.description + ? stripHtml(entry.description) + : "No description available."; + + const details = `${entry.name} + +${cleanDescription}`; + + const result = await copyToClipboard(details); + + if (result.success) { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + toast({ + title: "Details Copied!", + description: `${entry.name} name and description copied to clipboard.`, + }); + } else { + if (isInIframeUtil()) { + showCopyPrompt(details); + toast({ + title: "Manual Copy Required", + description: "Please copy the details from the popup dialog.", + }); + } else { + toast({ + title: "Failed to Copy Details", + description: result.error || "Could not copy the entry details.", + variant: "destructive", + }); + } + } + }; + + const handleCopyAll = async (event?: React.MouseEvent) => { + event?.stopPropagation(); + + const url = generateEntryUrl(entry.id, entry.type as "exicon" | "lexicon"); + const cleanDescription = entry.description + ? stripHtml(entry.description) + : "No description available."; + + const allContent = `${entry.name} + +${cleanDescription} + +${url}`; + + const result = await copyToClipboard(allContent); + + if (result.success) { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + toast({ + title: "All Details Copied!", + description: `${entry.name} name, description, and URL copied to clipboard.`, + }); + } else { + if (isInIframeUtil()) { + showCopyPrompt(allContent); + toast({ + title: "Manual Copy Required", + description: "Please copy the details from the popup dialog.", + }); + } else { + toast({ + title: "Failed to Copy", + description: result.error || "Could not copy the entry content.", + variant: "destructive", + }); + } + } + }; + + if (!showLabel && size === "icon") { + // Icon button with dropdown + return ( + + + + + e.stopPropagation()}> + + + Copy URL + + + + Copy Name & Description + + + + Copy All + + + + ); + } + + // Button with label and dropdown + return ( + + + + + e.stopPropagation()}> + + + Copy URL + + + + Copy Name & Description + + + + Copy All + + + + ); +} diff --git a/src/components/shared/EntryCard.tsx b/src/components/shared/EntryCard.tsx index a9b95a9..5f32f70 100644 --- a/src/components/shared/EntryCard.tsx +++ b/src/components/shared/EntryCard.tsx @@ -31,21 +31,16 @@ import { useState, useMemo } from "react"; import { SuggestionEditForm } from "@/components/submission/SuggestionEditForm"; import { useToast } from "@/hooks/use-toast"; import { getYouTubeEmbedUrl } from "@/lib/utils"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { copyToClipboard, isInIframe as isInIframeUtil, showCopyPrompt, } from "@/lib/clipboard"; -import { generateEntryUrl, getEntryBaseUrl } from "@/lib/route-utils"; +import { getEntryBaseUrl } from "@/lib/route-utils"; import { RichTextDisplay } from "@/components/shared/RichTextDisplay"; import { isHtmlContent } from "@/lib/sanitizeHtml"; import { convertPlainTextToHtml } from "@/lib/textToHtml"; +import { CopyEntryButton } from "@/components/shared/CopyEntryButton"; interface EntryCardProps { entry: AnyEntry & { @@ -317,33 +312,6 @@ export function EntryCard({ entry }: EntryCardProps) { } }; - const handleCopyEntryContent = async (event: React.MouseEvent) => { - event.stopPropagation(); - const url = generateEntryUrl(entry.id, entry.type as "exicon" | "lexicon"); - - const result = await copyToClipboard(url); - - if (result.success) { - toast({ - title: `${entry.name} URL Copied!`, - }); - } else { - // If all automatic methods fail, show manual copy prompt - if (isInIframeUtil()) { - showCopyPrompt(url); - toast({ - title: "Manual Copy Required", - description: "Please copy the link from the popup dialog.", - }); - } else { - toast({ - title: "Failed to Copy URL", - description: result.error || "Could not copy the entry URL.", - variant: "destructive", - }); - } - } - }; const videoLink = entry.type === "exicon" ? (entry as ExiconEntry).videoLink : undefined; @@ -368,24 +336,12 @@ export function EntryCard({ entry }: EntryCardProps) {

) : null} - - - - - - -

Copy Entry URL

-
-
-
+ @@ -524,6 +480,13 @@ export function EntryCard({ entry }: EntryCardProps) {
+