forked from minzeyaphyo/burmddit
- Replace @vercel/postgres with standard pg library - Add Dockerfile for Next.js standalone build - Add tsconfig.json, postcss.config.js - Fix globals.css undefined tailwind utilities - Force dynamic rendering for DB-dependent pages - Add .dockerignore
328 lines
13 KiB
TypeScript
328 lines
13 KiB
TypeScript
import { sql } from '@/lib/db'
|
||
import { notFound } from 'next/navigation'
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
import Link from 'next/link'
|
||
import Image from 'next/image'
|
||
|
||
async function getArticle(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
|
||
FROM articles a
|
||
JOIN categories c ON a.category_id = c.id
|
||
WHERE a.slug = ${slug} AND a.status = 'published'
|
||
`
|
||
|
||
if (rows.length === 0) return null
|
||
|
||
// Increment view count
|
||
await sql`SELECT increment_view_count(${slug})`
|
||
|
||
return rows[0]
|
||
} catch (error) {
|
||
console.error('Error fetching article:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
async function getRelatedArticles(articleId: number) {
|
||
try {
|
||
const { rows } = await sql`SELECT * FROM get_related_articles(${articleId}, 5)`
|
||
return rows
|
||
} catch (error) {
|
||
return []
|
||
}
|
||
}
|
||
|
||
export default async function ArticlePage({ params }: { params: { slug: string } }) {
|
||
const article = await getArticle(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="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
{/* Breadcrumb */}
|
||
<nav className="mb-6 text-sm">
|
||
<Link href="/" className="text-primary-600 hover:text-primary-700">
|
||
ပင်မစာမျက်နှာ
|
||
</Link>
|
||
<span className="mx-2 text-gray-400">/</span>
|
||
<Link
|
||
href={`/category/${article.category_slug}`}
|
||
className="text-primary-600 hover:text-primary-700 font-burmese"
|
||
>
|
||
{article.category_name_burmese}
|
||
</Link>
|
||
<span className="mx-2 text-gray-400">/</span>
|
||
<span className="text-gray-600 font-burmese">{article.title_burmese}</span>
|
||
</nav>
|
||
|
||
{/* Article Header */}
|
||
<article className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||
{/* Category Badge */}
|
||
<div className="p-6 pb-0">
|
||
<Link
|
||
href={`/category/${article.category_slug}`}
|
||
className="inline-block px-3 py-1 bg-primary-100 text-primary-700 rounded-full text-sm font-medium font-burmese mb-4 hover:bg-primary-200"
|
||
>
|
||
{article.category_name_burmese}
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Featured Image */}
|
||
{article.featured_image && (
|
||
<div className="relative h-96 w-full">
|
||
<Image
|
||
src={article.featured_image}
|
||
alt={article.title_burmese}
|
||
fill
|
||
className="object-cover"
|
||
priority
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Article Content */}
|
||
<div className="p-6 lg:p-12">
|
||
{/* Title */}
|
||
<h1 className="text-4xl font-bold text-gray-900 mb-4 font-burmese leading-tight">
|
||
{article.title_burmese}
|
||
</h1>
|
||
|
||
{/* Meta Info */}
|
||
<div className="flex items-center text-sm text-gray-600 mb-8 pb-8 border-b">
|
||
<span className="font-burmese">{publishedDate}</span>
|
||
<span className="mx-3">•</span>
|
||
<span className="font-burmese">{article.reading_time} မိနစ်</span>
|
||
<span className="mx-3">•</span>
|
||
<span className="font-burmese">{article.view_count} ကြည့်ရှုမှု</span>
|
||
</div>
|
||
|
||
{/* Article Body */}
|
||
<div className="article-content prose prose-lg max-w-none">
|
||
<div dangerouslySetInnerHTML={{ __html: formatContent(article.content_burmese) }} />
|
||
|
||
{/* 🔥 Additional Images Gallery */}
|
||
{article.images && article.images.length > 1 && (
|
||
<div className="mt-8 mb-8">
|
||
<h3 className="text-xl font-bold mb-4 font-burmese">ဓာတ်ပုံများ</h3>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||
{article.images.slice(1).map((img: string, idx: number) => (
|
||
<div key={idx} className="relative h-48 rounded-lg overflow-hidden">
|
||
<Image
|
||
src={img}
|
||
alt={`${article.title_burmese} - ဓာတ်ပုံ ${idx + 2}`}
|
||
fill
|
||
className="object-cover hover:scale-105 transition-transform duration-200"
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 🔥 Videos */}
|
||
{article.videos && article.videos.length > 0 && (
|
||
<div className="mt-8 mb-8">
|
||
<h3 className="text-xl font-bold mb-4 font-burmese">ဗီဒီယိုများ</h3>
|
||
<div className="space-y-4">
|
||
{article.videos.map((video: string, idx: number) => (
|
||
<div key={idx} className="relative aspect-video rounded-lg overflow-hidden bg-gray-900">
|
||
{renderVideo(video)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* ⭐ SOURCE ATTRIBUTION - THIS IS THE KEY PART! */}
|
||
{article.source_articles && article.source_articles.length > 0 && (
|
||
<div className="mt-12 pt-8 border-t-2 border-gray-200 bg-gray-50 p-6 rounded-lg">
|
||
<h3 className="text-xl font-bold text-gray-900 mb-4 font-burmese flex items-center">
|
||
<svg className="w-6 h-6 mr-2 text-primary-600" 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">
|
||
ဤဆောင်းပါးကို အောက်ပါမူရင်းသတင်းများမှ စုစည်း၍ မြန်မာဘာသာသို့ ပြန်ဆိုထားခြင်း ဖြစ်ပါသည်။ အားလုံးသော အကြွေးအရ မူရင်းစာရေးသူများနှင့် ထုတ်ပြန်သူများကို သက်ဆိုင်ပါသည်။
|
||
</p>
|
||
<ul className="space-y-3">
|
||
{article.source_articles.map((source: any, index: number) => (
|
||
<li key={index} className="bg-white p-4 rounded border border-gray-200 hover:border-primary-300 transition-colors">
|
||
<div className="flex items-start">
|
||
<span className="flex-shrink-0 w-6 h-6 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center text-sm font-bold mr-3">
|
||
{index + 1}
|
||
</span>
|
||
<div className="flex-1">
|
||
<a
|
||
href={source.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="text-primary-600 hover:text-primary-700 font-medium break-words"
|
||
>
|
||
{source.title}
|
||
</a>
|
||
{source.author && source.author !== 'Unknown' && (
|
||
<p className="text-sm text-gray-600 mt-1">
|
||
<span className="font-burmese">စာရေးသူ:</span> {source.author}
|
||
</p>
|
||
)}
|
||
<p className="text-xs text-gray-500 mt-1 break-all">
|
||
{source.url}
|
||
</p>
|
||
</div>
|
||
<a
|
||
href={source.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="ml-2 text-primary-600 hover:text-primary-700"
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded">
|
||
<p className="text-sm text-gray-700 font-burmese">
|
||
<strong>မှတ်ချက်:</strong> ဤဆောင်းပါးသည် သတင်းမျာ လုံးစုစည်းကာ ပြန်ဆိုထားခြင်း ဖြစ်ပါသည်။ အသေးစိတ် အချက်အလက်များနှင့် မူရင်းအကြောင်းအရာများအတွက် အထက်ပါမူရင်းသတင်းရင်းမြစ်များကို ကြည့်ရှုပါ။
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Disclaimer */}
|
||
<div className="mt-6 p-4 bg-gray-100 rounded text-sm text-gray-600 font-burmese">
|
||
<p>
|
||
<strong>ပြန်ဆိုသူမှတ်ချက်:</strong> ဤဆောင်းပါးကို AI နည်းပညာဖြင့် အင်္ဂလိပ်ဘာသာမှ မြန်မာဘာသာသို့ ပြန်ဆိုထားပါသည်။ နည်းပညာဝေါဟာရများကို တတ်နိုင်သမျှ အင်္ဂလိပ်ဘာသာဖြင့် ထားရှိထားပါသည်။
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
{/* Related Articles */}
|
||
{relatedArticles.length > 0 && (
|
||
<div className="mt-12">
|
||
<h2 className="text-2xl font-bold text-gray-900 mb-6 font-burmese">
|
||
ဆက်စပ်ဆောင်းပါးများ
|
||
</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
{relatedArticles.map((related: any) => (
|
||
<Link
|
||
key={related.id}
|
||
href={`/article/${related.slug}`}
|
||
className="bg-white rounded-lg shadow hover:shadow-lg transition-shadow p-4"
|
||
>
|
||
{related.featured_image && (
|
||
<div className="relative h-32 w-full mb-3 rounded overflow-hidden">
|
||
<Image
|
||
src={related.featured_image}
|
||
alt={related.title_burmese}
|
||
fill
|
||
className="object-cover"
|
||
/>
|
||
</div>
|
||
)}
|
||
<h3 className="font-semibold text-gray-900 font-burmese line-clamp-2 hover:text-primary-600">
|
||
{related.title_burmese}
|
||
</h3>
|
||
<p className="text-sm text-gray-600 font-burmese mt-2 line-clamp-2">
|
||
{related.excerpt_burmese}
|
||
</p>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function formatContent(content: string): string {
|
||
// Convert markdown-like formatting to HTML
|
||
// This is a simple implementation - you might want to use a proper markdown parser
|
||
let formatted = content
|
||
.replace(/\n\n/g, '</p><p>')
|
||
.replace(/## (.*?)\n/g, '<h2>$1</h2>')
|
||
.replace(/### (.*?)\n/g, '<h3>$1</h3>')
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||
|
||
return `<p>${formatted}</p>`
|
||
}
|
||
|
||
function renderVideo(videoUrl: string) {
|
||
// Extract YouTube video ID
|
||
let videoId = null
|
||
|
||
// Handle different YouTube URL formats
|
||
if (videoUrl.includes('youtube.com/watch')) {
|
||
const match = videoUrl.match(/v=([^&]+)/)
|
||
videoId = match ? match[1] : null
|
||
} else if (videoUrl.includes('youtu.be/')) {
|
||
const match = videoUrl.match(/youtu\.be\/([^?]+)/)
|
||
videoId = match ? match[1] : null
|
||
} else if (videoUrl.includes('youtube.com/embed/')) {
|
||
const match = videoUrl.match(/embed\/([^?]+)/)
|
||
videoId = match ? match[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
|
||
/>
|
||
)
|
||
}
|
||
|
||
// For other video formats, try generic iframe embed
|
||
return (
|
||
<iframe
|
||
src={videoUrl}
|
||
className="w-full h-full"
|
||
allowFullScreen
|
||
/>
|
||
)
|
||
}
|
||
|
||
export async function generateMetadata({ params }: { params: { slug: string } }) {
|
||
const article = await getArticle(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] : [],
|
||
},
|
||
}
|
||
}
|