diff --git a/messages/en.json b/messages/en.json index 9bbb8d1..2c124ce 100644 --- a/messages/en.json +++ b/messages/en.json @@ -266,6 +266,9 @@ "promptCopied": "Prompt copied to clipboard", "promptDuplicated": "Prompt duplicated to your studio", "versions": "versions", + "promptContent": "Prompt Content", + "tags": "Tags", + "author": "Author", "createdAt": "Created", "loadingPrompts": "Loading prompts...", "loadMore": "Load More", diff --git a/messages/zh.json b/messages/zh.json index bab673e..13b8cd7 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -266,6 +266,9 @@ "promptCopied": "提示词已复制到剪贴板", "promptDuplicated": "提示词已复制到您的工作室", "versions": "个版本", + "promptContent": "提示词内容", + "tags": "标签", + "author": "作者", "createdAt": "创建时间", "loadingPrompts": "加载提示词中...", "loadMore": "加载更多", diff --git a/src/components/plaza/PromptCard.tsx b/src/components/plaza/PromptCard.tsx index c5491b6..f9edf10 100644 --- a/src/components/plaza/PromptCard.tsx +++ b/src/components/plaza/PromptCard.tsx @@ -16,11 +16,11 @@ import { import { Copy, Download, - Eye, Calendar, Check } from 'lucide-react' import { formatDistanceToNow } from 'date-fns' +import { PromptDetailModal } from './PromptDetailModal' interface Prompt { id: string @@ -65,6 +65,7 @@ export function PromptCard({ prompt, onViewIncrement }: PromptCardProps) { 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] @@ -174,22 +175,20 @@ export function PromptCard({ prompt, onViewIncrement }: PromptCardProps) { onViewIncrement(prompt.id) setViewIncremented(true) } + setIsDetailModalOpen(true) } return ( - + -
+
{prompt.name} -
- - {prompt.stats?.viewCount || 0} -
{prompt.description && ( @@ -288,5 +287,13 @@ export function PromptCard({ prompt, onViewIncrement }: PromptCardProps) {
+ + {/* Detail Modal */} + setIsDetailModalOpen(false)} + /> + ) } \ No newline at end of file diff --git a/src/components/plaza/PromptDetailModal.tsx b/src/components/plaza/PromptDetailModal.tsx new file mode 100644 index 0000000..998b424 --- /dev/null +++ b/src/components/plaza/PromptDetailModal.tsx @@ -0,0 +1,325 @@ +'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 { + X, + Copy, + Download, + Calendar, + Check, + Tag, + User +} from 'lucide-react' +import { formatDistanceToNow } from 'date-fns' + +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 PromptDetailModalProps { + prompt: Prompt | null + isOpen: boolean + onClose: () => void +} + +export function PromptDetailModal({ + prompt, + isOpen, + onClose +}: PromptDetailModalProps) { + const t = useTranslations('plaza') + const tCommon = useTranslations('common') + const { user } = useAuth() + const [copied, setCopied] = useState(false) + const [duplicating, setDuplicating] = useState(false) + + if (!isOpen || !prompt) return null + + const latestVersion = prompt.versions[0] + const promptContent = latestVersion?.content || prompt.content + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(promptContent) + 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: promptContent, + description: prompt.description, + permissions: 'private', + userId: user.id, + tags: prompt.tags.map(tag => tag.name), + } + + 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 formatDate = (dateString: string) => { + return new Intl.DateTimeFormat('default', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(new Date(dateString)) + } + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+

+ {prompt.name} +

+ {prompt.description && ( +

+ {prompt.description} +

+ )} +
+ +
+ + {/* Content */} +
+ {/* Prompt Content */} +
+

+ {t('promptContent')} +

+
+
+                {promptContent}
+              
+
+
+ + {/* Tags */} + {prompt.tags.length > 0 && ( +
+

+ + {t('tags')} +

+
+ {prompt.tags.map((tag) => ( + + {tag.name} + + ))} +
+
+ )} + + {/* Author & Meta Info */} +
+
+

+ + {t('author')} +

+
+ +
+

+ {prompt.user.username || 'Anonymous'} +

+
+
+
+ +
+

+ + {t('createdAt')} +

+

+ {formatDate(prompt.createdAt)} +

+

+ ({formatDistanceToNow(new Date(prompt.createdAt), { addSuffix: true })}) +

+
+
+
+ + {/* Footer Actions */} +
+ + + +
+
+
+ ) +}