ai-wpa/app/topics/page.tsx

384 lines
12 KiB
TypeScript

import { Suspense } from 'react'
import { Metadata } from 'next'
import { Header } from '@/components/header'
import { Footer } from '@/components/footer'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination'
import { Search, BookOpen, Eye } from 'lucide-react'
import Link from 'next/link'
import Image from 'next/image'
import { ITopic } from '@/models/topic'
// Force dynamic rendering for real-time topic updates
export const dynamic = 'force-dynamic'
// SEO metadata for the topics page
export async function generateMetadata(): Promise<Metadata> {
return {
title: 'Topic - SiliconPin | Web Development Insights & Tutorials',
description:
'Discover the latest insights, tutorials, and stories from the SiliconPin community. Learn about web development, cloud computing, and cutting-edge technology trends.',
keywords: [
'web development',
'cloud computing',
'programming tutorials',
'tech insights',
'software engineering',
'SiliconPin topic',
'development tutorials',
],
authors: [{ name: 'SiliconPin Team' }],
openGraph: {
title: 'SiliconPin Topic - Web Development Insights',
description:
'Discover the latest insights, tutorials, and stories from the SiliconPin community.',
type: 'website',
siteName: 'SiliconPin',
},
twitter: {
card: 'summary_large_image',
title: 'SiliconPin Topic - Web Development Insights',
description:
'Discover the latest insights, tutorials, and stories from the SiliconPin community.',
},
alternates: {
canonical: '/topics',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
},
},
}
}
// Server-side data fetching
async function parseSearchParams(
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
) {
const params = await searchParams
return {
page: params.page ? parseInt(params.page as string) : 1,
tag: params.tag ? (params.tag as string) : undefined,
search: params.q ? (params.q as string) : undefined,
}
}
async function getTopics(searchParams: Promise<{ [key: string]: string | string[] | undefined }>) {
const { page, tag, search } = await parseSearchParams(searchParams)
try {
// Build API URL with parameters
const baseUrl =
process.env.NODE_ENV === 'production'
? `${process.env.NEXTAUTH_URL || 'https://siliconpin.com'}`
: 'http://localhost:4024'
const searchQuery = new URLSearchParams({
page: page.toString(),
limit: '6',
...(tag && { tag }),
...(search && { q: search }),
})
const response = await fetch(`${baseUrl}/api/topics?${searchQuery}`, {
headers: {
'Content-Type': 'application/json',
},
// Disable cache for dynamic data
cache: 'no-store',
})
if (!response.ok) {
throw new Error(`Failed to fetch topics: ${response.status}`)
}
const result = await response.json()
if (!result.success) {
throw new Error(result.error?.message || 'Failed to fetch topics')
}
return {
topics: result.data || [],
pagination: result.pagination || {
total: 0,
page,
limit: 6,
totalPages: 0,
},
}
} catch (error) {
console.error('Error fetching topics:', error)
// Fallback to empty state
return {
topics: [],
pagination: {
total: 0,
page,
limit: 6,
totalPages: 0,
},
}
}
}
// Get unique tags from topics for filtering
async function getTags(): Promise<Array<{ id: string; name: string }>> {
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/tags`, {
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
if (!response.ok) {
throw new Error(`Failed to fetch tags: ${response.status}`)
}
const result = await response.json()
if (!result.success) {
throw new Error(result.error?.message || 'Failed to fetch tags')
}
return result.data || []
} catch (error) {
console.error('Error fetching tags:', error)
// Fallback to empty array
return []
}
}
// Topic grid skeleton loader
function TopicGridSkeleton() {
return (
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i} className="overflow-hidden">
<Skeleton className="aspect-video w-full" />
<CardContent className="p-6">
<div className="flex items-center gap-4 mb-3">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-20" />
</div>
<Skeleton className="h-6 w-full mb-3" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4 mb-4" />
<div className="flex gap-2">
<Skeleton className="h-6 w-16" />
<Skeleton className="h-6 w-20" />
<Skeleton className="h-6 w-14" />
</div>
</CardContent>
</Card>
))}
</div>
)
}
export default async function TopicsPage({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const {
page: currentPage,
tag: selectedTag,
search: searchQuery,
} = await parseSearchParams(searchParams)
const { topics, pagination } = await getTopics(searchParams)
const tags = await getTags()
return (
<div className="min-h-screen bg-background">
<Header />
<section className="py-12 md:py-20">
<div className="container-custom">
<h1 className="section-heading mb-12 text-center">Topic Posts</h1>
{/* Search and filter section */}
<div className="mb-10 space-y-6">
<form action="/topics" method="GET" className="flex gap-2">
<Input
placeholder="Search posts..."
name="q"
defaultValue={searchQuery}
className="flex-1"
/>
<Button type="submit">
<Search className="mr-2 h-4 w-4" />
Search
</Button>
{searchQuery && (
<Button variant="ghost" asChild>
<Link href="/topics">Clear</Link>
</Button>
)}
</form>
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Link
key={tag.id}
href={`/blogs${selectedTag === tag.name ? '' : `?tag=${tag.name}`}${searchQuery ? `&q=${searchQuery}` : ''}`}
className="transition-colors"
>
<Badge
variant={selectedTag === tag.name ? 'default' : 'outline'}
className={
selectedTag === tag.name
? 'hover:bg-primary/90'
: 'hover:bg-primary/10 hover:text-primary'
}
>
{tag.name}
</Badge>
</Link>
))}
</div>
</div>
{/* Filter info */}
{(selectedTag || searchQuery) && (
<div className="mb-6 rounded-md bg-muted p-3 text-sm">
<p>
{pagination.total} post(s) found
{selectedTag && ` with tag: ${selectedTag}`}
{searchQuery && ` containing: "${searchQuery}"`}
</p>
</div>
)}
{/* Topic posts grid */}
<Suspense fallback={<TopicGridSkeleton />}>
{topics.length > 0 ? (
<div className="mb-10 grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
{topics.map((topic) => (
<Link
key={topic.id}
href={`/topics/${topic.slug}`}
className="topic-card group block"
>
<div className="aspect-video overflow-hidden">
<Image
src={topic.coverImage}
alt={topic.title}
width={400}
height={225}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
priority
/>
</div>
<div className="p-6">
<div className="mb-2 flex items-center justify-between text-sm text-muted-foreground">
<span>
{new Date(topic.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</span>
{(topic.views || 0) > 0 && (
<span className="flex items-center gap-1">
<Eye className="h-3 w-3" />
{topic.views?.toLocaleString()}
</span>
)}
</div>
<h2 className="mb-2 text-xl font-bold transition-colors group-hover:text-primary">
{topic.title}
</h2>
<p className="mb-4 line-clamp-3 text-muted-foreground">{topic.excerpt}</p>
<div className="flex flex-wrap gap-2">
{topic.tags.map((tag) => (
<Badge key={tag.id} variant="outline">
{tag.name}
</Badge>
))}
</div>
</div>
</Link>
))}
</div>
) : (
<div className="py-10 text-center">
<p className="text-xl text-muted-foreground">
No posts found matching your criteria.
</p>
<Button variant="outline" className="mt-4" asChild>
<Link href="/topics">Reset filters</Link>
</Button>
</div>
)}
{/* Pagination */}
{topics.length > 0 && pagination.totalPages > 1 && (
<Pagination>
<PaginationContent>
{currentPage > 1 && (
<PaginationItem>
<PaginationPrevious
href={`/topics?page=${currentPage - 1}${selectedTag ? `&tag=${selectedTag}` : ''}${searchQuery ? `&q=${searchQuery}` : ''}`}
size="default"
/>
</PaginationItem>
)}
{Array.from({ length: Math.min(pagination.totalPages, 5) }, (_, i) => {
const page = i + 1
return (
<PaginationItem key={page}>
<PaginationLink
href={`/topics?page=${page}${selectedTag ? `&tag=${selectedTag}` : ''}${searchQuery ? `&q=${searchQuery}` : ''}`}
isActive={page === currentPage}
size="default"
>
{page}
</PaginationLink>
</PaginationItem>
)
})}
{currentPage < pagination.totalPages && (
<PaginationItem>
<PaginationNext
href={`/topics?page=${currentPage + 1}${selectedTag ? `&tag=${selectedTag}` : ''}${searchQuery ? `&q=${searchQuery}` : ''}`}
size="default"
/>
</PaginationItem>
)}
</PaginationContent>
</Pagination>
)}
</Suspense>
</div>
</section>
<Footer />
</div>
)
}