Prmbr/src/components/plaza/PromptCard.tsx
2025-08-03 13:06:12 +08:00

299 lines
8.7 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { useAuth } from '@/hooks/useAuth'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { LegacyAvatar } from '@/components/ui/avatar'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@/components/ui/card'
import {
Copy,
Download,
Calendar,
Check
} from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
import { PromptDetailModal } from './PromptDetailModal'
interface Prompt {
id: string
name: string
content: string
description: string | null
createdAt: string
user: {
id: string
username: string | null
avatar: string | null
}
tags: Array<{
id: string
name: string
color: string
}>
versions: Array<{
content: string
version: number
createdAt: string
}>
stats?: {
viewCount: number
likeCount: number
rating: number | null
ratingCount: number
} | null
_count: {
versions: number
}
}
interface PromptCardProps {
prompt: Prompt
onViewIncrement: (promptId: string) => void
}
export function PromptCard({ prompt, onViewIncrement }: PromptCardProps) {
const t = useTranslations('plaza')
const { user } = useAuth()
const [copied, setCopied] = useState(false)
const [duplicating, setDuplicating] = useState(false)
const [viewIncremented, setViewIncremented] = useState(false)
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false)
const latestVersion = prompt.versions[0]
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(latestVersion?.content || prompt.content)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error('Failed to copy:', error)
}
}
const handleDuplicate = async () => {
if (!user) {
// Redirect to sign in
window.location.href = '/signin'
return
}
setDuplicating(true)
try {
const requestData = {
name: `${prompt.name} (Copy)`,
content: latestVersion?.content || prompt.content,
description: prompt.description,
permissions: 'private',
userId: user.id,
tags: prompt.tags.map(tag => tag.name),
}
console.log('Duplicating prompt with data:', requestData)
const response = await fetch('/api/prompts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData),
})
if (response.ok) {
// Create a temporary success notification
const notification = document.createElement('div')
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-md shadow-lg z-50 transition-all duration-300'
notification.textContent = t('promptDuplicated')
document.body.appendChild(notification)
setTimeout(() => {
notification.style.opacity = '0'
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification)
}
}, 300)
}, 2000)
} else {
// Create error notification
console.error('API response error:', response.status, response.statusText)
let errorMessage = 'Failed to duplicate prompt'
try {
const errorData = await response.json()
console.error('Error data:', errorData)
errorMessage = errorData.error || `HTTP ${response.status}: ${response.statusText}`
} catch (e) {
console.error('Failed to parse error response:', e)
errorMessage = `HTTP ${response.status}: ${response.statusText}`
}
const notification = document.createElement('div')
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-md shadow-lg z-50 transition-all duration-300'
notification.textContent = errorMessage
document.body.appendChild(notification)
setTimeout(() => {
notification.style.opacity = '0'
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification)
}
}, 300)
}, 3000)
}
} catch (error) {
console.error('Failed to duplicate:', error)
// Create error notification for network/other errors
const notification = document.createElement('div')
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-md shadow-lg z-50 transition-all duration-300'
notification.textContent = 'Network error. Please try again.'
document.body.appendChild(notification)
setTimeout(() => {
notification.style.opacity = '0'
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification)
}
}, 300)
}, 3000)
} finally {
setDuplicating(false)
}
}
const handleCardClick = () => {
if (!viewIncremented) {
onViewIncrement(prompt.id)
setViewIncremented(true)
}
setIsDetailModalOpen(true)
}
return (
<>
<Card
className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer animate-fade-in"
onClick={handleCardClick}
>
<CardHeader className="pb-3">
<div className="mb-2">
<CardTitle className="text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors">
{prompt.name}
</CardTitle>
</div>
{prompt.description && (
<CardDescription className="line-clamp-2">
{prompt.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="space-y-4">
{/* Content Preview */}
<div className="bg-muted/50 rounded-md p-3">
<p className="text-sm text-foreground line-clamp-3 font-mono">
{latestVersion?.content || prompt.content}
</p>
</div>
{/* Tags */}
{prompt.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{prompt.tags.slice(0, 3).map((tag) => (
<Badge
key={tag.id}
variant="secondary"
className="text-xs"
style={{ backgroundColor: `${tag.color}20`, color: tag.color }}
>
{tag.name}
</Badge>
))}
{prompt.tags.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{prompt.tags.length - 3}
</Badge>
)}
</div>
)}
{/* Author & Meta */}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-2 min-w-0 flex-shrink-0">
<LegacyAvatar
src={prompt.user.avatar || undefined}
alt={prompt.user.username || 'User'}
size={24}
className="w-6 h-6"
/>
<span className="truncate max-w-[100px]">
{prompt.user.username || 'Anonymous'}
</span>
</div>
<div className="flex items-center">
<Calendar className="h-3 w-3 mr-1" />
{formatDistanceToNow(new Date(prompt.createdAt), { addSuffix: true })}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 pt-2 border-t">
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleCopy()
}}
className="flex-1"
>
{copied ? (
<>
<Check className="h-4 w-4 mr-2" />
{t('promptCopied')}
</>
) : (
<>
<Copy className="h-4 w-4 mr-2" />
{t('copyToClipboard')}
</>
)}
</Button>
<Button
variant="default"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDuplicate()
}}
disabled={duplicating}
className="flex-1"
>
<Download className="h-4 w-4 mr-2" />
{duplicating ? '...' : t('duplicateToStudio')}
</Button>
</div>
</CardContent>
</Card>
{/* Detail Modal */}
<PromptDetailModal
prompt={prompt}
isOpen={isDetailModalOpen}
onClose={() => setIsDetailModalOpen(false)}
/>
</>
)
}