ai-wpa/app/topics/[slug]/page.tsx

290 lines
9.2 KiB
TypeScript

import { notFound } from 'next/navigation'
import { ArrowLeft, Eye, Clock } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import Link from 'next/link'
import Image from 'next/image'
import { ITopic } from '@/models/topic'
import { Metadata } from 'next'
import { Header } from '@/components/header'
import { Footer } from '@/components/footer'
import { TopicViewTracker } from '@/components/topics/TopicClientComponents'
import { TopicEditButton } from '@/components/topics/TopicEditButton'
import { SimpleShareButtons } from '@/components/topics/SimpleShareButtons'
// Force dynamic rendering for real-time updates
export const dynamic = 'force-dynamic'
// Server-side data fetching
async function getTopicPost(slug: string): Promise<ITopic | null> {
try {
const baseUrl =
process.env.NODE_ENV === 'production'
? `${process.env.NEXTAUTH_URL || 'https://siliconpin.com'}`
: 'http://localhost:4024'
const response = await fetch(`${baseUrl}/api/topics/${encodeURIComponent(slug)}`, {
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
if (!response.ok) {
if (response.status === 404) {
return null
}
throw new Error(`Failed to fetch topic post: ${response.status}`)
}
const result = await response.json()
if (!result.success) {
if (result.error?.code === 'POST_NOT_FOUND') {
return null
}
throw new Error(result.error?.message || 'Failed to fetch topic post')
}
return result.data
} catch (error) {
console.error('Error fetching topic post:', error)
return null
}
}
async function getRelatedPosts(slug: string, limit: number = 2): Promise<ITopic[]> {
try {
const baseUrl =
process.env.NODE_ENV === 'production'
? `${process.env.NEXTAUTH_URL || 'https://siliconpin.com'}`
: 'http://localhost:4024'
const response = await fetch(
`${baseUrl}/api/topics/${encodeURIComponent(slug)}/related?limit=${limit}`,
{
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
}
)
if (!response.ok) {
throw new Error(`Failed to fetch related posts: ${response.status}`)
}
const result = await response.json()
if (!result.success) {
throw new Error(result.error?.message || 'Failed to fetch related posts')
}
return result.data || []
} catch (error) {
console.error('Error fetching related posts:', error)
return []
}
}
// Generate metadata for the page
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await getTopicPost(slug)
if (!post) {
return {
title: 'Topic Not Found - SiliconPin',
description: 'The requested topic could not be found.',
}
}
const keywords = post.tags.map((tag) => tag.name).join(', ')
return {
title: `${post.title} - SiliconPin Topics`,
description: post.excerpt,
keywords: keywords,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: new Date(post.publishedAt).toISOString(),
authors: [post.author],
images: post.coverImage ? [post.coverImage] : undefined,
tags: post.tags.map((tag) => tag.name),
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: post.coverImage ? [post.coverImage] : undefined,
},
}
}
export default async function TopicPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getTopicPost(slug)
if (!post) {
notFound()
}
const relatedPosts = await getRelatedPosts(slug)
const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<div className="min-h-screen bg-background">
<Header />
{/* View tracking only - no visual component */}
<TopicViewTracker topicSlug={post.slug} isDraft={post.isDraft || false} />
<article className="pt-20 pb-12">
<div className="container-custom max-w-4xl">
{/* Back to topics and Edit button */}
<div className="mb-8 flex items-center justify-between">
<Button variant="ghost" asChild className="pl-0 transition-all duration-200 hover:pl-2">
<Link href="/topics" className="flex items-center gap-2">
<ArrowLeft className="h-4 w-4" />
Back to all topics
</Link>
</Button>
<TopicEditButton topicId={post.id} authorId={post.authorId} />
</div>
{/* Header */}
<header className="mb-10">
<h1 className="mb-6 text-4xl font-bold md:text-5xl">{post.title}</h1>
<div className="mb-8 flex flex-wrap items-center gap-x-6 gap-y-2 text-muted-foreground">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
{post.author.charAt(0)}
</div>
<span>{post.author}</span>
</div>
<time dateTime={new Date(post.publishedAt).toISOString()}>{publishedDate}</time>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>
{post.readingTime?.text || `${post.readingTime?.minutes || 1} min read`}
</span>
</span>
{(post.views || 0) > 0 && (
<span className="flex items-center gap-1">
<Eye className="w-4 h-4" />
<span>{post.views?.toLocaleString()} views</span>
</span>
)}
</div>
<div className="mb-8 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Link key={tag.id || tag.name} href={`/topics?tag=${tag.name}`}>
<Badge variant="outline" className="hover:bg-secondary">
{tag.name}
</Badge>
</Link>
))}
</div>
</header>
{/* Cover Image */}
<div className="mb-10 overflow-hidden rounded-xl">
<Image
width={848}
height={560}
src={post.coverImage}
alt={post.title}
className="h-auto w-full object-cover"
priority
/>
</div>
{/* Content */}
<div className="prose prose-lg max-w-none">
<div className="mb-4" dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
{/* Tags */}
<div className="mt-12 border-t pt-8">
<h3 className="mb-4 text-lg font-semibold">Tags</h3>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Link key={tag.id || tag.name} href={`/topics?tag=${tag.name}`}>
<Badge variant="outline" className="hover:bg-secondary">
{tag.name}
</Badge>
</Link>
))}
</div>
{/* Share Buttons */}
<SimpleShareButtons title={post.title} url={`/topics/${post.slug}`} />
</div>
</div>
</article>
{/* Related posts */}
{relatedPosts.length > 0 && (
<section className="bg-muted/30 py-12">
<div className="container-custom max-w-4xl">
<h2 className="mb-8 text-2xl font-bold">Related Posts</h2>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{relatedPosts.map((relatedPost) => (
<Link
key={relatedPost.id}
href={`/topics/${relatedPost.slug}`}
className="topic-card group block"
>
<div className="aspect-video overflow-hidden">
<Image
width={406}
height={228}
src={relatedPost.coverImage}
alt={relatedPost.title}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
<div className="p-6">
<h3 className="mb-2 text-xl font-bold transition-colors group-hover:text-primary">
{relatedPost.title}
</h3>
<p className="mb-4 line-clamp-2 text-muted-foreground">{relatedPost.excerpt}</p>
<div className="flex flex-wrap gap-2">
{relatedPost.tags.slice(0, 2).map((tag) => (
<Badge key={tag.id || tag.name} variant="outline">
{tag.name}
</Badge>
))}
</div>
</div>
</Link>
))}
</div>
</div>
</section>
)}
<Footer />
</div>
)
}