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) {
+