290 lines
9.2 KiB
TypeScript
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>
|
|
)
|
|
}
|