diff --git a/src/components/prompts/AiToolBadge.test.tsx b/src/components/prompts/AiToolBadge.test.tsx new file mode 100644 index 0000000..27b3b21 --- /dev/null +++ b/src/components/prompts/AiToolBadge.test.tsx @@ -0,0 +1,39 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { AiToolBadge } from "./AiToolBadge"; + +describe("AiToolBadge", () => { + it("renders abbreviated label initially with full tool name in title and aria-label", () => { + render(); + + const badge = screen.getByRole("button", { name: "AI tool: Stable Diffusion" }); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveAttribute("title", "Stable Diffusion"); + expect(badge).toHaveTextContent("SD"); + }); + + it("toggles to full tool name on tap/click without needing hover/tooltip", () => { + render(); + + const badge = screen.getByRole("button", { name: "AI tool: NANO BANANA (Gemini)" }); + expect(badge).toHaveTextContent("Nano Banana"); + + // Tap/click to reveal full name + fireEvent.click(badge); + expect(badge).toHaveTextContent("NANO BANANA (Gemini)"); + expect(badge).toHaveAttribute("aria-expanded", "true"); + + // Tap again to toggle back + fireEvent.click(badge); + expect(badge).toHaveTextContent("Nano Banana"); + expect(badge).toHaveAttribute("aria-expanded", "false"); + }); + + it("renders custom tool names with fallback gracefully", () => { + render(); + + const badge = screen.getByRole("button", { name: "AI tool: Custom AI Engine" }); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent("Custom AI Engine"); + }); +}); diff --git a/src/components/prompts/AiToolBadge.tsx b/src/components/prompts/AiToolBadge.tsx index 93110f2..a0cb15c 100644 --- a/src/components/prompts/AiToolBadge.tsx +++ b/src/components/prompts/AiToolBadge.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; import { AdobeIcon, GeminiIcon, MetaIcon, OpenAIIcon } from "./toolIcons"; @@ -45,21 +46,39 @@ interface AiToolBadgeProps { } export function AiToolBadge({ tool, className }: AiToolBadgeProps) { + const [showFull, setShowFull] = useState(false); // "Other" lets people type anything, so an unknown name is expected, not a bug. const { label, icon: Icon, emoji } = PRESENTATION[tool] ?? { label: tool, icon: Sparkles }; + const displayText = showFull ? tool : label; return ( - // The full name stays in the tooltip — the label is abbreviated, and on a - // card there is no room to spell out "NANO BANANA (Gemini)". - + // The full name is findable via title tooltip on desktop, via tap/click + // toggle on touch devices, and via aria-label for assistive tech. + ); } diff --git a/src/components/prompts/PromptCard.test.tsx b/src/components/prompts/PromptCard.test.tsx index 4e9b038..1ce3ee2 100644 --- a/src/components/prompts/PromptCard.test.tsx +++ b/src/components/prompts/PromptCard.test.tsx @@ -71,6 +71,46 @@ describe("PromptCard", () => { expect(ratingElement).toHaveTextContent("Not rated"); }); + it("renders mobile drawer with complete actions including Copy Prompt and Like", () => { + renderPromptCard(baseProps); + + const [mobileTrigger] = screen.getAllByLabelText("More options"); + fireEvent.click(mobileTrigger); + + // Mobile drawer contains all primary actions + expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Like" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Share" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Link" })).toBeInTheDocument(); + expect(screen.getByText("View Profile")).toBeInTheDocument(); + expect(screen.getByText("AI Tool:")).toBeInTheDocument(); + expect(screen.getAllByText("Midjourney").length).toBeGreaterThan(0); + }); + + it("renders desktop dropdown with complete actions including Copy Prompt and Like", () => { + renderPromptCard(baseProps); + + const [, desktopTrigger] = screen.getAllByLabelText("More options"); + fireEvent.pointerDown(desktopTrigger, { button: 0, ctrlKey: false }); + fireEvent.keyDown(desktopTrigger, { key: "ArrowDown" }); + + expect(screen.getByText("Copy Prompt")).toBeInTheDocument(); + expect(screen.getByText("Like")).toBeInTheDocument(); + expect(screen.getByText("Save")).toBeInTheDocument(); + expect(screen.getByText("Share")).toBeInTheDocument(); + expect(screen.getByText("Copy Link")).toBeInTheDocument(); + expect(screen.getByText("View Profile")).toBeInTheDocument(); + expect(screen.getByText("Tool:")).toBeInTheDocument(); + }); + + it("includes touch hover-none override on copy button for touch screens like iPad Pro (#45)", () => { + renderPromptCard(baseProps); + + const copyButton = screen.getByLabelText("Copy prompt"); + expect(copyButton.className).toContain("[@media(hover:none)]:!opacity-100"); + }); + it("applies snappy transition tokens to card and image", () => { renderPromptCard(baseProps); diff --git a/src/components/prompts/PromptCard.tsx b/src/components/prompts/PromptCard.tsx index f234721..a38fec1 100644 --- a/src/components/prompts/PromptCard.tsx +++ b/src/components/prompts/PromptCard.tsx @@ -4,7 +4,7 @@ import { RatingInvitation } from "@/components/prompts/RatingInvitation"; import { useCopyRatingInvitation } from "@/hooks/useCopyRatingInvitation"; import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { Eye, Copy, Heart, Bookmark, Check, Pencil, Trash2, Share2, MoreHorizontal, Link as LinkIcon, UserCircle, Flag, MoreVertical, Star } from "lucide-react"; +import { Eye, Copy, Heart, Bookmark, Check, Pencil, Trash2, Share2, MoreHorizontal, Link as LinkIcon, UserCircle, Flag, MoreVertical, Star, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; import { useAuth } from "@/hooks/useAuth"; import { useToast } from "@/hooks/use-toast"; @@ -131,9 +131,9 @@ export function PromptCard({ navigate(`/prompt/${id}#accuracy-rating`); }; - const handleCopy = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleCopy = async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); if (!user) { askToSignIn("copy prompts"); @@ -160,9 +160,9 @@ export function PromptCard({ setTimeout(() => setCopied(false), 2000); }; - const handleLike = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleLike = async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); if (!user) { askToSignIn("like prompts"); @@ -172,9 +172,9 @@ export function PromptCard({ likeMutation.toggle(localLiked, localLikeCount); }; - const handleSave = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleSave = async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); if (!user) { askToSignIn("save prompts"); @@ -184,15 +184,15 @@ export function PromptCard({ saveMutation.toggle(localSaved); }; - const handleCopyLink = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleCopyLink = async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); await copyPromptLink(id); }; - const handleShare = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleShare = (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); setShareOpen(true); }; @@ -256,15 +256,15 @@ export function PromptCard({ e.preventDefault(); e.stopPropagation(); }} - className="absolute top-2 sm:top-3 right-2 sm:right-3 lg:hidden z-30 pointer-events-auto" + className="absolute top-2 sm:top-3 right-2 sm:right-3 md:hidden z-30 pointer-events-auto" > @@ -273,46 +273,89 @@ export function PromptCard({ Post options Actions for this prompt - {/* Primary Actions - Large Circular Buttons */} -
- {/* Save Button */} + {/* Primary Actions - Circular Buttons */} +
+ {/* Copy Prompt Button */} - {/* Copy Link Button */} + {/* Like Button */} + + {/* Save Button */} + {/* Share Button - one drawer closes before the other opens */} @@ -323,12 +366,26 @@ export function PromptCard({ setMobileMenuOpen(false); setTimeout(() => setShareOpen(true), 250); }} - className="flex flex-col items-center gap-2" + className="flex flex-col items-center gap-1.5 min-w-0" > -
- +
+
- Share + Share + + + {/* Copy Link Button */} +
@@ -337,6 +394,22 @@ export function PromptCard({ {/* Secondary Actions - List Items */}
+ {/* Edit for owner on profile */} + {showEditButton && onEditClick && ( + + )} + {/* View Profile */} Report )} + + {/* AI Tool Info */} +
+ + AI Tool: {toolUsed} +
- {/* Copy button - top LEFT on all devices, visible on hover for desktop */} + {/* Copy button - top LEFT on all devices, visible on hover for desktop, always visible on touch */}