Prmbr/src/components/plaza/PlazaClient.tsx
2025-09-02 00:11:45 +08:00

336 lines
9.6 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { useTranslations } from 'next-intl'
import { Header } from '@/components/layout/Header'
import { PlazaFilters } from './PlazaFilters'
import { PromptCard } from './PromptCard'
import { SimulatorCard } from './SimulatorCard'
import { Button } from '@/components/ui/button'
import { Loader2, FileText, Play } from 'lucide-react'
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 SimulatorRun {
id: string
name: string
status: string
userInput: string
output?: string | null
createdAt: string
user: {
id: string
username: string | null
image: string | null
}
prompt: {
id: string
name: string
content: string
tags: Array<{
id: string
name: string
color: string
}>
}
model: {
name: string
provider: string
outputType?: string
description?: string
}
}
interface PlazaResponse {
prompts?: Prompt[]
simulatorRuns?: SimulatorRun[]
type: 'prompts' | 'simulators' | 'all'
pagination: {
page: number
limit: number
total: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
}
interface Tag {
id: string
name: string
color: string
_count: {
prompts: number
}
}
export function PlazaClient() {
const t = useTranslations('plaza')
const [contentType, setContentType] = useState<'prompts' | 'simulators'>('prompts')
const [prompts, setPrompts] = useState<Prompt[]>([])
const [simulatorRuns, setSimulatorRuns] = useState<SimulatorRun[]>([])
const [tags, setTags] = useState<Tag[]>([])
const [loading, setLoading] = useState(true)
const [loadingMore, setLoadingMore] = useState(false)
const [search, setSearch] = useState('')
const [selectedTag, setSelectedTag] = useState('')
const [sortBy, setSortBy] = useState('createdAt')
const [sortOrder, setSortOrder] = useState('desc')
const [pagination, setPagination] = useState({
page: 1,
limit: 12,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
})
const fetchContent = useCallback(async (page = 1, reset = false) => {
if (page === 1) setLoading(true)
else setLoadingMore(true)
try {
const params = new URLSearchParams({
type: contentType,
page: page.toString(),
limit: pagination.limit.toString(),
sortBy,
sortOrder,
})
if (search) params.append('search', search)
if (selectedTag && contentType === 'prompts') params.append('tag', selectedTag)
const response = await fetch(`/api/plaza?${params}`)
if (!response.ok) throw new Error('Failed to fetch')
const data: PlazaResponse = await response.json()
if (reset) {
if (data.type === 'prompts' && data.prompts) {
setPrompts(data.prompts)
setSimulatorRuns([])
} else if (data.type === 'simulators' && data.simulatorRuns) {
setSimulatorRuns(data.simulatorRuns)
setPrompts([])
}
} else {
if (data.type === 'prompts' && data.prompts) {
setPrompts(prev => [...prev, ...data.prompts!])
} else if (data.type === 'simulators' && data.simulatorRuns) {
setSimulatorRuns(prev => [...prev, ...data.simulatorRuns!])
}
}
setPagination(data.pagination)
} catch (error) {
console.error('Error fetching content:', error)
} finally {
setLoading(false)
setLoadingMore(false)
}
}, [contentType, search, selectedTag, sortBy, sortOrder, pagination.limit])
const fetchTags = useCallback(async () => {
try {
const response = await fetch('/api/plaza/tags')
if (!response.ok) throw new Error('Failed to fetch tags')
const data = await response.json()
setTags(data.tags)
} catch (error) {
console.error('Error fetching tags:', error)
}
}, [])
useEffect(() => {
fetchTags()
}, [fetchTags])
useEffect(() => {
fetchContent(1, true)
}, [contentType, search, selectedTag, sortBy, sortOrder, fetchContent])
const handleLoadMore = () => {
if (pagination.hasNext && !loadingMore) {
fetchContent(pagination.page + 1, false)
}
}
const handleClearFilters = () => {
setSearch('')
setSelectedTag('')
setSortBy('createdAt')
setSortOrder('desc')
}
const incrementViewCount = async (promptId: string) => {
try {
await fetch(`/api/plaza/${promptId}/view`, {
method: 'POST',
})
} catch (error) {
console.error('Error updating view count:', error)
}
}
return (
<div className="min-h-screen bg-background">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-foreground mb-4">
广
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
</p>
</div>
{/* Content Type Toggle */}
<div className="flex justify-center mb-8">
<div className="inline-flex rounded-lg border border-border p-1 bg-muted/50">
<button
onClick={() => setContentType('prompts')}
className={`inline-flex items-center px-4 py-2 text-sm font-medium rounded-md transition-all ${
contentType === 'prompts'
? 'bg-background text-foreground shadow-sm border border-border'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<FileText className="h-4 w-4 mr-2" />
</button>
<button
onClick={() => setContentType('simulators')}
className={`inline-flex items-center px-4 py-2 text-sm font-medium rounded-md transition-all ${
contentType === 'simulators'
? 'bg-background text-foreground shadow-sm border border-border'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Play className="h-4 w-4 mr-2" />
</button>
</div>
</div>
{/* Filters */}
<PlazaFilters
search={search}
onSearchChange={setSearch}
selectedTag={selectedTag}
onTagChange={setSelectedTag}
tags={tags}
sortBy={sortBy}
onSortByChange={setSortBy}
sortOrder={sortOrder}
onSortOrderChange={setSortOrder}
onClearFilters={handleClearFilters}
/>
{/* Results */}
<div className="mb-6">
<p className="text-sm text-muted-foreground">
{contentType === 'prompts' ? prompts.length : simulatorRuns.length} / {pagination.total} {contentType === 'prompts' ? '提示词' : '运行结果'}
</p>
</div>
{/* Content */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">...</span>
</div>
) : (contentType === 'prompts' ? prompts.length > 0 : simulatorRuns.length > 0) ? (
<>
{/* Content Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{contentType === 'prompts'
? prompts.map((prompt) => (
<PromptCard
key={prompt.id}
prompt={prompt}
onViewIncrement={incrementViewCount}
/>
))
: simulatorRuns.map((simulatorRun) => (
<SimulatorCard
key={simulatorRun.id}
simulatorRun={simulatorRun}
onViewIncrement={incrementViewCount}
/>
))
}
</div>
{/* Load More */}
{pagination.hasNext && (
<div className="text-center">
<Button
onClick={handleLoadMore}
disabled={loadingMore}
variant="outline"
size="lg"
>
{loadingMore ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
...
</>
) : (
'加载更多'
)}
</Button>
</div>
)}
</>
) : (
<div className="text-center py-12">
<h3 className="text-lg font-medium text-foreground mb-2">
{contentType === 'prompts' ? '提示词' : '运行结果'}
</h3>
<p className="text-muted-foreground mb-4">
</p>
<Button variant="outline" onClick={handleClearFilters}>
</Button>
</div>
)}
</main>
</div>
)
}