This commit is contained in:
2026-01-16 16:59:48 +08:00
9 changed files with 1364 additions and 1248 deletions
+161
View File
@@ -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 }
)
}
}
+12 -8
View File
@@ -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
-42
View File
@@ -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 */}