Files
burmddit/frontend/app/article/[slug]/page.tsx
Min Zeya Phyo 964afce761 UI updates
2026-02-26 15:07:05 +06:30

293 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { sql } from '@/lib/db'
export const dynamic = "force-dynamic"
import { notFound } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
import ShareButtons from '@/components/ShareButtons'
async function getArticleWithTags(slug: string) {
try {
const { rows } = await sql`
SELECT
a.*,
c.name as category_name,
c.name_burmese as category_name_burmese,
c.slug as category_slug,
COALESCE(
array_agg(t.name_burmese) FILTER (WHERE t.id IS NOT NULL),
ARRAY[]::VARCHAR[]
) as tags_burmese,
COALESCE(
array_agg(t.slug) FILTER (WHERE t.id IS NOT NULL),
ARRAY[]::VARCHAR[]
) as tag_slugs
FROM articles a
JOIN categories c ON a.category_id = c.id
LEFT JOIN article_tags at ON a.id = at.article_id
LEFT JOIN tags t ON at.tag_id = t.id
WHERE a.slug = ${slug} AND a.status = 'published'
GROUP BY a.id, c.id
`
if (rows.length === 0) return null
// Increment view count only here (not in generateMetadata)
await sql`SELECT increment_view_count(${slug})`
return rows[0]
} catch (error) {
console.error('Error fetching article:', error)
return null
}
}
// Separate metadata fetch — no view count increment
async function getArticleMeta(slug: string) {
try {
const { rows } = await sql`
SELECT title_burmese, excerpt_burmese, featured_image
FROM articles
WHERE slug = ${slug} AND status = 'published'
LIMIT 1
`
return rows[0] || null
} catch {
return null
}
}
async function getRelatedArticles(articleId: number) {
try {
const { rows } = await sql`SELECT * FROM get_related_articles(${articleId}, 6)`
return rows
} catch {
return []
}
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await getArticleWithTags(params.slug)
if (!article) notFound()
const relatedArticles = await getRelatedArticles(article.id)
const publishedDate = new Date(article.published_at).toLocaleDateString('my-MM', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<div className="min-h-screen bg-white">
{/* Hero Cover Image */}
{article.featured_image && (
<div className="relative h-[55vh] w-full overflow-hidden">
<Image
src={article.featured_image}
alt={article.title_burmese}
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/50 to-black/10" />
<div className="absolute inset-0 flex items-end">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-10 w-full">
<Link
href={`/category/${article.category_slug}`}
className="inline-block mb-3 px-4 py-1.5 bg-primary rounded-full text-white font-semibold text-sm hover:bg-primary-dark transition-colors font-burmese"
>
{article.category_name_burmese}
</Link>
<h1 className="text-2xl md:text-3xl lg:text-4xl font-bold text-white mb-4 font-burmese leading-snug line-clamp-3">
{article.title_burmese}
</h1>
<div className="flex flex-wrap items-center gap-3 text-white/80 text-sm">
<span className="font-burmese">{publishedDate}</span>
<span></span>
<span className="font-burmese">{article.reading_time} </span>
<span></span>
<span>{article.view_count} views</span>
</div>
</div>
</div>
</div>
)}
{/* Article Content */}
<article className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Tags */}
{article.tags_burmese && article.tags_burmese.length > 0 && (
<div className="flex flex-wrap gap-2 mb-8 pb-8 border-b border-gray-100">
{article.tags_burmese.map((tag: string, idx: number) => (
<Link
key={idx}
href={`/tag/${article.tag_slugs[idx]}`}
className="tag tag-burmese"
>
#{tag}
</Link>
))}
</div>
)}
{/* Body */}
<div className="article-content">
<div dangerouslySetInnerHTML={{ __html: formatContent(article.content_burmese) }} />
{/* Image Gallery */}
{article.images && article.images.length > 1 && (
<div className="my-10 not-prose">
<h3 className="text-xl font-bold mb-4 font-burmese text-gray-900"></h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{article.images.slice(1).map((img: string, idx: number) => (
<div key={idx} className="relative h-48 rounded-xl overflow-hidden image-zoom">
<Image src={img} alt={`${article.title_burmese} ${idx + 2}`} fill className="object-cover" />
</div>
))}
</div>
</div>
)}
{/* Videos */}
{article.videos && article.videos.length > 0 && (
<div className="my-10 not-prose">
<h3 className="text-xl font-bold mb-4 font-burmese text-gray-900"></h3>
<div className="space-y-6">
{article.videos.map((video: string, idx: number) => (
<div key={idx} className="relative aspect-video rounded-xl overflow-hidden bg-gray-900 shadow-xl">
{renderVideo(video)}
</div>
))}
</div>
</div>
)}
</div>
{/* Source Attribution */}
{article.source_articles && article.source_articles.length > 0 && (
<div className="mt-12 p-6 bg-blue-50 rounded-2xl border border-blue-100">
<h3 className="text-lg font-bold text-gray-900 mb-3 font-burmese flex items-center gap-2">
<svg className="w-5 h-5 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</h3>
<p className="text-sm text-gray-600 mb-4 font-burmese leading-relaxed">
က က
</p>
<div className="space-y-3">
{article.source_articles.map((source: any, index: number) => (
<div key={index} className="bg-white p-4 rounded-xl border border-gray-100 flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-xs font-bold">
{index + 1}
</span>
<div className="flex-1 min-w-0">
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm font-medium break-words"
>
{source.title}
</a>
{source.author && source.author !== 'Unknown' && (
<p className="text-xs text-gray-500 mt-1 font-burmese">
: {source.author}
</p>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Share */}
<div className="mt-10 py-8 border-t border-gray-100">
<p className="font-burmese text-gray-700 font-semibold mb-4">:</p>
<ShareButtons title={article.title_burmese} />
</div>
</article>
{/* Related Articles */}
{relatedArticles.length > 0 && (
<section className="bg-gray-50 py-14">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-gray-900 mb-8 font-burmese">
က
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{relatedArticles.map((related: any) => (
<Link key={related.id} href={`/article/${related.slug}`} className="card card-hover">
{related.featured_image && (
<div className="relative h-44 w-full image-zoom">
<Image src={related.featured_image} alt={related.title_burmese} fill className="object-cover" />
</div>
)}
<div className="p-5">
<h3 className="font-bold text-gray-900 font-burmese line-clamp-3 hover:text-primary transition-colors leading-snug mb-2">
{related.title_burmese}
</h3>
<p className="text-sm text-gray-500 font-burmese line-clamp-2 leading-relaxed">
{related.excerpt_burmese}
</p>
</div>
</Link>
))}
</div>
</div>
</section>
)}
</div>
)
}
function formatContent(content: string): string {
const formatted = content
.replace(/### (.*?)(\n|$)/g, '<h3>$1</h3>')
.replace(/## (.*?)(\n|$)/g, '<h2>$1</h2>')
.replace(/# (.*?)(\n|$)/g, '<h1>$1</h1>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n\n+/g, '</p><p>')
.replace(/\n/g, '<br/>')
return `<p>${formatted}</p>`
}
function renderVideo(videoUrl: string) {
let videoId: string | null = null
if (videoUrl.includes('youtube.com/watch')) {
const m = videoUrl.match(/v=([^&]+)/)
videoId = m ? m[1] : null
} else if (videoUrl.includes('youtu.be/')) {
const m = videoUrl.match(/youtu\.be\/([^?]+)/)
videoId = m ? m[1] : null
} else if (videoUrl.includes('youtube.com/embed/')) {
const m = videoUrl.match(/embed\/([^?]+)/)
videoId = m ? m[1] : null
}
if (videoId) {
return (
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
className="w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
)
}
return null
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const article = await getArticleMeta(params.slug)
if (!article) return { title: 'Article Not Found' }
return {
title: `${article.title_burmese} - Burmddit`,
description: article.excerpt_burmese,
openGraph: {
title: article.title_burmese,
description: article.excerpt_burmese,
images: article.featured_image ? [article.featured_image] : [],
},
}
}