Initial Burmddit deployment - AI news aggregator in Burmese

This commit is contained in:
Zeya Phyo
2026-02-19 02:52:58 +00:00
commit dddb86ea94
27 changed files with 5039 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
import { sql } from '@vercel/postgres'
import { notFound } from 'next/navigation'
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] : [],
},
}
}

82
frontend/app/globals.css Normal file
View File

@@ -0,0 +1,82 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* Burmese font support */
@font-face {
font-family: 'Pyidaungsu';
src: url('https://myanmar-tools-website.appspot.com/fonts/Pyidaungsu-2.5.3_Regular.ttf') format('truetype');
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'Pyidaungsu';
src: url('https://myanmar-tools-website.appspot.com/fonts/Pyidaungsu-2.5.3_Bold.ttf') format('truetype');
font-weight: 700;
font-display: swap;
}
/* Article content styling */
.article-content {
@apply font-burmese text-gray-800 leading-relaxed;
}
.article-content h1 {
@apply text-3xl font-bold mt-8 mb-4;
}
.article-content h2 {
@apply text-2xl font-bold mt-6 mb-3;
}
.article-content h3 {
@apply text-xl font-semibold mt-4 mb-2;
}
.article-content p {
@apply mb-4 text-lg leading-loose;
}
.article-content a {
@apply text-primary-600 hover:text-primary-700 underline;
}
.article-content ul, .article-content ol {
@apply ml-6 mb-4 space-y-2;
}
.article-content li {
@apply text-lg;
}
.article-content code {
@apply bg-gray-100 px-2 py-1 rounded text-sm font-mono;
}
.article-content pre {
@apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-4;
}
.article-content blockquote {
@apply border-l-4 border-primary-500 pl-4 italic my-4;
}
/* Card hover effects */
.article-card {
@apply transition-transform duration-200 hover:scale-105 hover:shadow-xl;
}
/* Loading skeleton */
.skeleton {
@apply animate-pulse bg-gray-200 rounded;
}

36
frontend/app/layout.tsx Normal file
View File

@@ -0,0 +1,36 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import Header from '@/components/Header'
import Footer from '@/components/Footer'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Burmddit - Myanmar AI News & Tutorials',
description: 'Daily AI news, tutorials, and tips in Burmese. Stay updated with the latest in artificial intelligence.',
keywords: 'AI, Myanmar, Burmese, AI news, AI tutorials, machine learning, ChatGPT',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="my" className="font-burmese">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Myanmar:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body className={`${inter.className} bg-gray-50`}>
<Header />
<main className="min-h-screen">
{children}
</main>
<Footer />
</body>
</html>
)
}

124
frontend/app/page.tsx Normal file
View File

@@ -0,0 +1,124 @@
import { sql } from '@vercel/postgres'
import ArticleCard from '@/components/ArticleCard'
import TrendingSection from '@/components/TrendingSection'
import CategoryNav from '@/components/CategoryNav'
async function getRecentArticles() {
try {
const { rows } = await sql`
SELECT * FROM published_articles
ORDER BY published_at DESC
LIMIT 20
`
return rows
} catch (error) {
console.error('Error fetching articles:', error)
return []
}
}
async function getTrendingArticles() {
try {
const { rows } = await sql`SELECT * FROM get_trending_articles(10)`
return rows
} catch (error) {
console.error('Error fetching trending:', error)
return []
}
}
export default async function Home() {
const [articles, trending] = await Promise.all([
getRecentArticles(),
getTrendingArticles()
])
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Hero Section */}
<section className="mb-12 text-center">
<h1 className="text-5xl font-bold text-gray-900 mb-4 font-burmese">
Burmddit
</h1>
<p className="text-xl text-gray-600 font-burmese">
AI ကက
</p>
<p className="text-lg text-gray-500 mt-2">
Daily AI News, Tutorials & Tips in Burmese
</p>
</section>
{/* Category Navigation */}
<CategoryNav />
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
{/* Main Articles (Left 2/3) */}
<div className="lg:col-span-2">
<h2 className="text-2xl font-bold text-gray-900 mb-6 font-burmese">
က
</h2>
{articles.length === 0 ? (
<div className="text-center py-12 bg-white rounded-lg shadow">
<p className="text-gray-500 font-burmese">
က က
</p>
</div>
) : (
<div className="space-y-6">
{articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
</div>
)}
</div>
{/* Sidebar (Right 1/3) */}
<aside className="space-y-8">
{/* Trending Articles */}
<TrendingSection articles={trending} />
{/* Categories Card */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4 font-burmese">
</h3>
<ul className="space-y-2">
<li>
<a href="/category/ai-news" className="text-primary-600 hover:text-primary-700 font-burmese">
AI
</a>
</li>
<li>
<a href="/category/tutorials" className="text-primary-600 hover:text-primary-700 font-burmese">
</a>
</li>
<li>
<a href="/category/tips-tricks" className="text-primary-600 hover:text-primary-700 font-burmese">
ကက
</a>
</li>
<li>
<a href="/category/upcoming" className="text-primary-600 hover:text-primary-700 font-burmese">
</a>
</li>
</ul>
</div>
{/* About Card */}
<div className="bg-gradient-to-br from-primary-50 to-primary-100 rounded-lg shadow p-6">
<h3 className="text-lg font-bold text-gray-900 mb-3 font-burmese">
Burmddit က
</h3>
<p className="text-gray-700 text-sm leading-relaxed font-burmese">
Burmddit AI က က ကကက က
</p>
</div>
</aside>
</div>
</div>
)
}