299 lines
8.7 KiB
TypeScript
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)}
|
|
/>
|
|
</>
|
|
)
|
|
} |