- 新增 MarkdownContent 组件支持 Markdown 渲染 - 新增 scripts 目录下的项目添加工具脚本 - 更新项目详情页面支持 Markdown 内容显示 - 更新依赖包支持 Markdown 解析 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
184 lines
7.0 KiB
TypeScript
184 lines
7.0 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { MarkdownContent } from './MarkdownContent'
|
|
|
|
interface ProjectDetailProps {
|
|
project: {
|
|
id: string
|
|
name: string
|
|
nameEn?: string | null
|
|
slug: string
|
|
description: string
|
|
descriptionEn?: string | null
|
|
content?: string | null
|
|
contentEn?: string | null
|
|
source?: string | null
|
|
createdAt: Date
|
|
tags: Array<{
|
|
id: string
|
|
name: string
|
|
nameEn?: string | null
|
|
slug: string
|
|
}>
|
|
links: Array<{
|
|
id: string
|
|
type: string
|
|
url: string
|
|
title?: string | null
|
|
}>
|
|
}
|
|
locale: string
|
|
}
|
|
|
|
// Helper function to format date
|
|
function formatDate(date: Date | string, locale: string): string {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date
|
|
const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' }
|
|
return dateObj.toLocaleDateString(locale === 'en' ? 'en-US' : 'zh-CN', options)
|
|
}
|
|
|
|
export function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
|
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
|
|
const displayDescription =
|
|
locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
|
|
const displayContent = locale === 'en' && project.contentEn ? project.contentEn : project.content
|
|
|
|
// Get category from first tag
|
|
const category = project.tags[0]?.name || 'AI Agent'
|
|
const categoryEn = project.tags[0]?.nameEn || project.tags[0]?.name || 'AI Agent'
|
|
const displayCategory = locale === 'en' ? categoryEn : category
|
|
|
|
const handleShare = () => {
|
|
if (typeof window !== 'undefined') {
|
|
const url = encodeURIComponent(window.location.href)
|
|
const text = encodeURIComponent(`Check out ${displayName} on Agent Park`)
|
|
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank')
|
|
}
|
|
}
|
|
|
|
const handleCopyLink = () => {
|
|
if (typeof window !== 'undefined') {
|
|
navigator.clipboard.writeText(window.location.href)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* Header Section */}
|
|
<header className="mb-10 relative">
|
|
{/* Decorative shape */}
|
|
<div
|
|
className="absolute -top-10 -right-10 w-24 h-24 border border-black dark:border-white opacity-20 rounded-full hidden md:block pointer-events-none"
|
|
style={{ borderRadius: '40% 60% 70% 30% / 40% 50% 60% 50%' }}
|
|
></div>
|
|
|
|
<h1 className="font-display text-4xl md:text-6xl font-bold mb-6 leading-tight uppercase relative z-10">
|
|
{displayName}
|
|
</h1>
|
|
|
|
{/* Metadata */}
|
|
<div className="flex flex-wrap items-center gap-4 text-sm font-mono text-gray-600 dark:text-gray-400 mb-8 pb-8 border-b border-gray-300 dark:border-gray-700">
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-icons text-base">calendar_today</span>
|
|
<span>Added {formatDate(project.createdAt, locale)}</span>
|
|
</div>
|
|
<span className="hidden sm:inline text-gray-300">|</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-icons text-base">category</span>
|
|
<span>{displayCategory}</span>
|
|
</div>
|
|
<span className="hidden sm:inline text-gray-300">|</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-icons text-base">code</span>
|
|
<span>Open Source</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
<div className="flex flex-wrap gap-3 mb-8">
|
|
{project.tags.map((tag) => (
|
|
<span
|
|
key={tag.id}
|
|
className="px-3 py-1 border border-black dark:border-gray-500 text-xs font-display font-bold uppercase bg-white dark:bg-gray-800"
|
|
>
|
|
{tag.name}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
{/* Featured Image/Video Placeholder */}
|
|
<div className="relative w-full aspect-video border-2 border-black dark:border-gray-600 bg-gray-100 dark:bg-gray-800 mb-10 overflow-hidden shadow-brutal dark:shadow-brutal-dark flex items-center justify-center">
|
|
{project.source ? (
|
|
<iframe
|
|
src={project.source}
|
|
className="w-full h-full"
|
|
title={`${displayName} demo`}
|
|
allowFullScreen
|
|
/>
|
|
) : (
|
|
<div className="text-center">
|
|
<span className="material-icons text-6xl text-gray-400 dark:text-gray-600 mb-2">
|
|
smart_toy
|
|
</span>
|
|
<p className="text-gray-500 dark:text-gray-400 font-display text-sm">Project Preview</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Article Content */}
|
|
<article className="max-w-4xl mx-auto">
|
|
{/* Lead paragraph */}
|
|
<p className="lead font-display text-xl mb-8 text-gray-700 dark:text-gray-300">
|
|
{displayDescription}
|
|
</p>
|
|
|
|
{/* Full content with Markdown rendering */}
|
|
{displayContent && <MarkdownContent content={displayContent} />}
|
|
|
|
{/* Installation section if GitHub link exists */}
|
|
{project.links.some((l) => l.type === 'GITHUB') && (
|
|
<>
|
|
<h3>Getting Started</h3>
|
|
<p>To install this project, clone the repository and install dependencies:</p>
|
|
<pre className="bg-gray-100 dark:bg-gray-800 border border-black dark:border-gray-600 p-4 font-mono text-sm overflow-x-auto">
|
|
<code>{`git clone ${
|
|
project.links.find((l) => l.type === 'GITHUB')?.url || 'https://github.com/example/project'
|
|
}
|
|
cd ${project.slug}
|
|
npm install`}</code>
|
|
</pre>
|
|
</>
|
|
)}
|
|
</article>
|
|
|
|
{/* Share and Feedback Section */}
|
|
<div className="mt-12 pt-8 border-t border-gray-300 dark:border-gray-700 flex flex-col sm:flex-row justify-between items-center gap-6">
|
|
<div className="flex gap-4">
|
|
<button
|
|
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
title="Share on X"
|
|
onClick={handleShare}
|
|
>
|
|
<span className="material-icons text-lg">share</span>
|
|
</button>
|
|
<button
|
|
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
title="Copy Link"
|
|
onClick={handleCopyLink}
|
|
>
|
|
<span className="material-icons text-lg">link</span>
|
|
</button>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-display font-bold text-gray-500">DID THIS AGENT HELP YOU?</span>
|
|
<button className="px-4 py-2 bg-primary text-black font-display font-bold text-xs uppercase border border-black hover:bg-yellow-400 transition-colors shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:shadow-none active:translate-x-[2px] active:translate-y-[2px]">
|
|
Yes, it rocks
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|