Merge branch 'main' of https://github.com/Mzaxd/agent_park
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
/**
|
||||
* DELETE /api/projects/[slug]
|
||||
*
|
||||
* 根据项目的 slug 删除项目及其所有关联数据
|
||||
*
|
||||
* 由于数据库 schema 配置了 onDelete: Cascade,
|
||||
* 删除项目时会自动删除:
|
||||
* - 该项目的所有外部链接(ExternalLink)
|
||||
* - 该项目的所有标签关联(ProjectTag)
|
||||
*
|
||||
* 注意:Tag 本身不会被删除,只会删除项目与标签的关联关系
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params
|
||||
|
||||
// Verify API Key
|
||||
const apiKey = request.headers.get('x-api-key') || process.env.WEBHOOK_API_KEY
|
||||
const validApiKey = process.env.WEBHOOK_API_KEY
|
||||
|
||||
if (apiKey !== validApiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
details: ['Invalid or missing API Key'],
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const existingProject = await prisma.project.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
links: true,
|
||||
tags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!existingProject) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Not Found',
|
||||
details: [`Project with slug "${slug}" not found`],
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Delete project (cascade delete will handle links and project_tags)
|
||||
await prisma.project.delete({
|
||||
where: { slug },
|
||||
})
|
||||
|
||||
console.warn(
|
||||
`[API] Deleted project "${existingProject.name}" (slug: ${slug}, id: ${existingProject.id})`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Project deleted successfully',
|
||||
data: {
|
||||
project: {
|
||||
id: existingProject.id,
|
||||
name: existingProject.name,
|
||||
nameEn: existingProject.nameEn,
|
||||
slug: existingProject.slug,
|
||||
},
|
||||
deleted: {
|
||||
linksCount: existingProject.links.length,
|
||||
tagsCount: existingProject.tags.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[API] Error deleting project:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
details: [error instanceof Error ? error.message : 'Unknown error'],
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/projects/[slug]
|
||||
*
|
||||
* 根据项目的 slug 获取项目详情
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params
|
||||
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
links: true,
|
||||
tags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!project) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Not Found',
|
||||
details: [`Project with slug "${slug}" not found`],
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Transform response to match frontend structure
|
||||
const transformedProject = {
|
||||
...project,
|
||||
tags: project.tags.map((pt) => ({
|
||||
id: pt.tag.id,
|
||||
name: pt.tag.name,
|
||||
nameEn: pt.tag.nameEn,
|
||||
slug: pt.tag.slug,
|
||||
})),
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: transformedProject,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[API] Error fetching project:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
details: [error instanceof Error ? error.message : 'Unknown error'],
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -213,15 +213,19 @@ const components: Components = {
|
||||
</a>
|
||||
),
|
||||
|
||||
// Images
|
||||
// Images - wrapped in container for size control
|
||||
img: ({ src, alt, ...props }) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="rounded-lg border border-gray-300 dark:border-gray-600 my-4 max-w-full h-auto"
|
||||
loading="lazy"
|
||||
{...props}
|
||||
/>
|
||||
<div className="my-4 flex justify-center">
|
||||
<div className="max-w-md w-full">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="rounded-lg border border-gray-300 dark:border-gray-600 w-full h-auto object-contain"
|
||||
loading="lazy"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
// Horizontal rule
|
||||
|
||||
@@ -48,13 +48,6 @@ function formatDate(date: Date | string, locale: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if URL is an image
|
||||
function isImageUrl(url: string): boolean {
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']
|
||||
const lowerUrl = url.toLowerCase()
|
||||
return imageExtensions.some(ext => lowerUrl.includes(ext))
|
||||
}
|
||||
|
||||
export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
const t = await getTranslations('project')
|
||||
|
||||
@@ -114,26 +107,6 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Featured Image/Video Placeholder - only show if source exists */}
|
||||
{project.source && (
|
||||
<div className="relative w-full 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">
|
||||
{isImageUrl(project.source) ? (
|
||||
<img
|
||||
src={project.source}
|
||||
alt={`${displayName} demo`}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
) : (
|
||||
<iframe
|
||||
src={project.source}
|
||||
className="w-full aspect-video"
|
||||
title={`${displayName} demo`}
|
||||
allowFullScreen
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Article Content */}
|
||||
@@ -145,21 +118,6 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
|
||||
{/* Full content with Markdown rendering */}
|
||||
{displayContent && <MarkdownContent content={displayContent} />}
|
||||
|
||||
{/* Installation section if GitHub link exists */}
|
||||
{project.links.some((l) => l.type === 'GITHUB') && (
|
||||
<>
|
||||
<h3>{t('gettingStarted')}</h3>
|
||||
<p>{t('installInstructions')}</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 - Temporarily disabled for debugging */}
|
||||
|
||||
Reference in New Issue
Block a user