From 343b8156b4a370a6ac42fa23318f5fcb665998c0 Mon Sep 17 00:00:00 2001 From: rahulkr182 Date: Tue, 8 Sep 2026 11:46:14 +0530 Subject: [PATCH 1/2] fix(prompts): unify card actions across screen widths and align breakpoints (fixes #98, fixes #45) --- src/components/prompts/AiToolBadge.test.tsx | 39 ++++ src/components/prompts/AiToolBadge.tsx | 29 ++- src/components/prompts/PromptCard.test.tsx | 42 +++- src/components/prompts/PromptCard.tsx | 224 +++++++++++++++----- src/hooks/use-mobile.test.tsx | 21 ++ src/hooks/use-mobile.tsx | 2 +- 6 files changed, 291 insertions(+), 66 deletions(-) create mode 100644 src/components/prompts/AiToolBadge.test.tsx create mode 100644 src/hooks/use-mobile.test.tsx 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..dcaa4b0 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,37 @@ 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 5d3c220..d745f68 100644 --- a/src/components/prompts/PromptCard.test.tsx +++ b/src/components/prompts/PromptCard.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -71,4 +71,44 @@ describe("PromptCard", () => { expect(ratingElement).toBeInTheDocument(); 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"); + }); }); diff --git a/src/components/prompts/PromptCard.tsx b/src/components/prompts/PromptCard.tsx index 3d39b21..495de26 100644 --- a/src/components/prompts/PromptCard.tsx +++ b/src/components/prompts/PromptCard.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Link } from "react-router-dom"; -import { Copy, Heart, Bookmark, Check, Pencil, Trash2, Share2, MoreHorizontal, Link as LinkIcon, UserCircle, Flag, MoreVertical, Star } from "lucide-react"; +import { 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"; @@ -106,9 +106,9 @@ export function PromptCard({ // Only the creator can delete; everyone else gets Report in that slot. const isOwner = !!user && !!profile && profile.id === creator.id; - const handleCopy = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleCopy = async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); if (!user) { onLoginRequired?.(); @@ -125,9 +125,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) { toast({ @@ -151,9 +151,9 @@ export function PromptCard({ onLikeChange?.(); }; - const handleSave = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleSave = async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); if (!user) { toast({ @@ -252,15 +252,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" > @@ -269,46 +269,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 */} @@ -319,12 +362,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 + + + {/* Copy Link Button */} +
@@ -333,6 +390,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 */}