Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/app/exicon/[entryId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -200,7 +200,12 @@ export default async function ExiconEntryPage({
)}

<div className="flex justify-end gap-2">
<CopyEntryUrlButton entry={exiconEntry} />
<CopyEntryButton
entry={exiconEntry}
variant="outline"
size="sm"
showLabel={true}
/>
<SuggestEditsButton entry={exiconEntry} />
</div>
</CardContent>
Expand Down
9 changes: 7 additions & 2 deletions src/app/lexicon/[entryId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -137,7 +137,12 @@ export default async function LexiconEntryPage({
</div>

<div className="flex justify-end gap-2">
<CopyEntryUrlButton entry={lexiconEntry} />
<CopyEntryButton
entry={lexiconEntry}
variant="outline"
size="sm"
showLabel={true}
/>
<SuggestEditsButton entry={lexiconEntry} />
</div>
</CardContent>
Expand Down
226 changes: 226 additions & 0 deletions src/components/shared/CopyEntryButton.tsx
Original file line number Diff line number Diff line change
@@ -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(/&nbsp;/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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant={variant}
size={size}
className={`h-8 w-8 text-muted-foreground hover:text-accent ${className}`}
aria-label="Copy entry options"
onClick={(e) => e.stopPropagation()}
>
{copied ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={handleCopyUrl}>
<Copy className="mr-2 h-4 w-4" />
Copy URL
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCopyDetails}>
<Copy className="mr-2 h-4 w-4" />
Copy Name & Description
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCopyAll}>
<Copy className="mr-2 h-4 w-4" />
Copy All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

// Button with label and dropdown
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant={variant}
size={size}
className={className}
onClick={(e) => e.stopPropagation()}
>
{copied ? (
<>
<Check className="mr-2 h-4 w-4 text-green-600" />
Copied!
</>
) : (
<>
<Copy className="mr-2 h-4 w-4" />
Copy
<ChevronDown className="ml-1 h-3 w-3" />
</>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={handleCopyUrl}>
<Copy className="mr-2 h-4 w-4" />
Copy URL
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCopyDetails}>
<Copy className="mr-2 h-4 w-4" />
Copy Name & Description
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCopyAll}>
<Copy className="mr-2 h-4 w-4" />
Copy All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
67 changes: 15 additions & 52 deletions src/components/shared/EntryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -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;
Expand All @@ -368,24 +336,12 @@ export function EntryCard({ entry }: EntryCardProps) {
</p>
) : null}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleCopyEntryContent}
className="ml-auto flex-shrink-0 h-8 w-8 text-muted-foreground hover:text-accent"
aria-label="Copy entry content"
>
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy Entry URL</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<CopyEntryButton
entry={entry}
variant="ghost"
size="icon"
className="ml-auto flex-shrink-0"
/>
</div>
</CardHeader>

Expand Down Expand Up @@ -524,6 +480,13 @@ export function EntryCard({ entry }: EntryCardProps) {
</div>

<div className="flex-shrink-0 pt-4 border-t flex flex-col sm:flex-row justify-end gap-2">
<CopyEntryButton
entry={entry}
variant="outline"
size="default"
showLabel={true}
className="w-full sm:w-auto"
/>
<Dialog
open={isSuggestEditFormOpen}
onOpenChange={setIsSuggestEditFormOpen}
Expand Down
Loading