updated for languagees

This commit is contained in:
chandresh 2026-02-11 22:18:36 +05:30
parent cb8b768d3c
commit 26a617e1d4
4 changed files with 1385 additions and 314 deletions

View File

@ -0,0 +1,49 @@
import { pool } from '../config/database.js'
import dotenv from 'dotenv'
dotenv.config()
async function up() {
try {
console.log('Running add-language-column migration...')
// Add language column
await pool.query(`
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS language VARCHAR(10) DEFAULT 'en' NOT NULL;
`)
console.log('✓ Added language column')
// Add post_group_id to link English and Hindi versions
await pool.query(`
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS post_group_id UUID NULL;
`)
console.log('✓ Added post_group_id column')
// Create indexes
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_posts_language ON posts(language);
`)
console.log('✓ Created language index')
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_posts_group_id ON posts(post_group_id);
`)
console.log('✓ Created post_group_id index')
// Backfill existing posts to English
await pool.query(`
UPDATE posts SET language = 'en' WHERE language IS NULL;
`)
console.log('✓ Backfilled existing posts with language=en')
console.log('✓ add-language-column migration completed successfully')
process.exit(0)
} catch (err) {
console.error('Migration failed:', err)
process.exit(1)
}
}
up()

View File

@ -19,7 +19,7 @@ function isValidExternalUrl(url) {
router.get('/', async (req, res) => {
try {
logger.transaction('FETCH_POSTS', { userId: req.user.id })
const query = 'SELECT id, title, slug, status, content_type, external_url, thumbnail_url, excerpt, created_at, updated_at FROM posts WHERE user_id = $1 ORDER BY updated_at DESC'
const query = 'SELECT id, title, slug, status, content_type, external_url, thumbnail_url, excerpt, language, post_group_id, created_at, updated_at FROM posts WHERE user_id = $1 ORDER BY updated_at DESC'
logger.db('SELECT', query, [req.user.id])
const result = await pool.query(query, [req.user.id])
@ -94,13 +94,14 @@ router.get('/slug/:slug', async (req, res) => {
// Create post
router.post('/', async (req, res) => {
try {
const { title, content_json, content_type, external_url, status, thumbnail_url, excerpt } = req.body
const { title, content_json, content_type, external_url, status, thumbnail_url, excerpt, language } = req.body
logger.transaction('CREATE_POST', {
userId: req.user.id,
title: title?.substring(0, 50),
status: status || 'draft',
content_type: content_type || 'tiptap'
content_type: content_type || 'tiptap',
language: language || 'en'
})
const isLinkPost = content_type === 'link'
@ -127,6 +128,7 @@ router.post('/', async (req, res) => {
const contentType = isLinkPost ? 'link' : (content_type || 'tiptap')
const contentJson = isLinkPost ? {} : content_json
const externalUrl = isLinkPost ? external_url.trim() : null
const postLanguage = language || 'en'
const thumbnailUrl = thumbnail_url && typeof thumbnail_url === 'string' ? thumbnail_url.trim() || null : null
const excerptVal = excerpt != null && typeof excerpt === 'string' ? excerpt.trim().slice(0, 500) || null : null
@ -140,10 +142,10 @@ router.post('/', async (req, res) => {
}
}
const query = `INSERT INTO posts (user_id, title, content_json, slug, status, content_type, external_url, thumbnail_url, excerpt)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
const query = `INSERT INTO posts (user_id, title, content_json, slug, status, content_type, external_url, thumbnail_url, excerpt, language)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`
logger.db('INSERT', query, [req.user.id, title, '[content_json]', slug, postStatus, contentType, externalUrl, thumbnailUrl, excerptVal])
logger.db('INSERT', query, [req.user.id, title, '[content_json]', slug, postStatus, contentType, externalUrl, thumbnailUrl, excerptVal, postLanguage])
const result = await pool.query(query, [
req.user.id,
@ -154,13 +156,15 @@ router.post('/', async (req, res) => {
contentType,
externalUrl,
thumbnailUrl,
excerptVal
excerptVal,
postLanguage
])
logger.transaction('CREATE_POST_SUCCESS', {
postId: result.rows[0].id,
userId: req.user.id,
slug: result.rows[0].slug
slug: result.rows[0].slug,
language: postLanguage
})
res.status(201).json(result.rows[0])
} catch (error) {
@ -172,7 +176,7 @@ router.post('/', async (req, res) => {
// Update post
router.put('/:id', async (req, res) => {
try {
const { title, content_json, content_type, external_url, status, thumbnail_url, excerpt } = req.body
const { title, content_json, content_type, external_url, status, thumbnail_url, excerpt, language } = req.body
logger.transaction('UPDATE_POST', {
postId: req.params.id,
@ -184,7 +188,8 @@ router.put('/:id', async (req, res) => {
content_type: content_type !== undefined,
external_url: external_url !== undefined,
thumbnail_url: thumbnail_url !== undefined,
excerpt: excerpt !== undefined
excerpt: excerpt !== undefined,
language: language !== undefined
}
})
@ -279,6 +284,11 @@ router.put('/:id', async (req, res) => {
values.push(excerpt != null && typeof excerpt === 'string' ? excerpt.trim().slice(0, 500) || null : null)
}
if (language !== undefined) {
updates.push(`language = $${paramCount++}`)
values.push(language || 'en')
}
updates.push(`updated_at = NOW()`)
values.push(req.params.id, req.user.id)
@ -330,4 +340,393 @@ router.delete('/:id', async (req, res) => {
}
})
// Get post group by post_group_id (returns both EN and HI versions)
router.get('/group/:post_group_id', async (req, res) => {
try {
logger.transaction('FETCH_POST_GROUP', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
const query = 'SELECT * FROM posts WHERE post_group_id = $1 AND user_id = $2 ORDER BY language'
logger.db('SELECT', query, [req.params.post_group_id, req.user.id])
const result = await pool.query(query, [req.params.post_group_id, req.user.id])
if (result.rows.length === 0) {
logger.warn('POSTS', 'Post group not found', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
return res.status(404).json({ message: 'Post group not found' })
}
// Organize by language
const posts = {
post_group_id: req.params.post_group_id,
english: result.rows.find(p => p.language === 'en') || null,
hindi: result.rows.find(p => p.language === 'hi') || null
}
logger.transaction('FETCH_POST_GROUP_SUCCESS', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
res.json(posts)
} catch (error) {
logger.error('POSTS', 'Error fetching post group', error)
res.status(500).json({ message: 'Failed to fetch post group', error: error.message })
}
})
// Create dual-language post (creates both EN and HI posts together)
router.post('/dual-language', async (req, res) => {
try {
const {
// English fields
title_en, excerpt_en, content_json_en, external_url_en, status_en,
// Hindi fields
title_hi, excerpt_hi, content_json_hi, external_url_hi, status_hi,
// Shared fields
content_type, thumbnail_url, status
} = req.body
// Support both old format (status) and new format (status_en/status_hi)
const enStatus = status_en || status || 'draft'
const hiStatus = status_hi || status || 'draft'
logger.transaction('CREATE_DUAL_LANGUAGE_POST', {
userId: req.user.id,
status_en: enStatus,
status_hi: hiStatus,
content_type: content_type || 'tiptap'
})
const isLinkPost = content_type === 'link'
// Validation for publishing English
if (enStatus === 'published') {
if (!title_en?.trim() || !excerpt_en?.trim()) {
return res.status(400).json({ message: 'English title and excerpt required to publish' })
}
if (isLinkPost && !external_url_en?.trim()) {
return res.status(400).json({ message: 'English URL required for link posts' })
}
if (isLinkPost && !isValidExternalUrl(external_url_en)) {
return res.status(400).json({ message: 'Valid English external URL is required' })
}
if (!isLinkPost && !content_json_en) {
return res.status(400).json({ message: 'English content required' })
}
if (!thumbnail_url?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Validation for publishing Hindi
if (hiStatus === 'published') {
if (!title_hi?.trim() || !excerpt_hi?.trim()) {
return res.status(400).json({ message: 'Hindi title and excerpt required to publish' })
}
if (isLinkPost && !external_url_hi?.trim()) {
return res.status(400).json({ message: 'Hindi URL required for link posts' })
}
if (isLinkPost && !isValidExternalUrl(external_url_hi)) {
return res.status(400).json({ message: 'Valid Hindi external URL is required' })
}
if (!isLinkPost && !content_json_hi) {
return res.status(400).json({ message: 'Hindi content required' })
}
if (!thumbnail_url?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Generate shared post_group_id using crypto
const { randomUUID } = await import('crypto')
const postGroupId = randomUUID()
const contentType = isLinkPost ? 'link' : (content_type || 'tiptap')
const thumbnailUrlVal = thumbnail_url && typeof thumbnail_url === 'string' ? thumbnail_url.trim() || null : null
// Create English post
const slugEn = slugify(title_en || 'untitled', { lower: true, strict: true }) + '-' + Date.now()
const excerptEnVal = excerpt_en != null && typeof excerpt_en === 'string' ? excerpt_en.trim().slice(0, 500) || null : null
const contentEnVal = isLinkPost ? {} : (content_json_en || {})
const externalUrlEnVal = isLinkPost && external_url_en ? external_url_en.trim() : null
const enQuery = `INSERT INTO posts (user_id, title, content_json, slug, status, content_type, external_url, thumbnail_url, excerpt, language, post_group_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *`
logger.db('INSERT', enQuery, [req.user.id, title_en, '[content_json]', slugEn, enStatus, contentType, externalUrlEnVal, thumbnailUrlVal, excerptEnVal, 'en', postGroupId])
const enResult = await pool.query(enQuery, [
req.user.id,
title_en || 'Untitled',
JSON.stringify(contentEnVal),
slugEn,
enStatus,
contentType,
externalUrlEnVal,
thumbnailUrlVal,
excerptEnVal,
'en',
postGroupId
])
// Create Hindi post
const slugHi = slugify(title_hi || 'untitled', { lower: true, strict: true }) + '-' + Date.now() + '-hi'
const excerptHiVal = excerpt_hi != null && typeof excerpt_hi === 'string' ? excerpt_hi.trim().slice(0, 500) || null : null
const contentHiVal = isLinkPost ? {} : (content_json_hi || {})
const externalUrlHiVal = isLinkPost && external_url_hi ? external_url_hi.trim() : null
const hiQuery = `INSERT INTO posts (user_id, title, content_json, slug, status, content_type, external_url, thumbnail_url, excerpt, language, post_group_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *`
logger.db('INSERT', hiQuery, [req.user.id, title_hi, '[content_json]', slugHi, hiStatus, contentType, externalUrlHiVal, thumbnailUrlVal, excerptHiVal, 'hi', postGroupId])
const hiResult = await pool.query(hiQuery, [
req.user.id,
title_hi || 'शीर्षकहीन',
JSON.stringify(contentHiVal),
slugHi,
hiStatus,
contentType,
externalUrlHiVal,
thumbnailUrlVal,
excerptHiVal,
'hi',
postGroupId
])
logger.transaction('CREATE_DUAL_LANGUAGE_POST_SUCCESS', {
postGroupId: postGroupId,
userId: req.user.id,
englishId: enResult.rows[0].id,
hindiId: hiResult.rows[0].id
})
res.status(201).json({
post_group_id: postGroupId,
english: enResult.rows[0],
hindi: hiResult.rows[0]
})
} catch (error) {
logger.error('POSTS', 'Error creating dual-language post', error)
res.status(500).json({ message: 'Failed to create posts', error: error.message })
}
})
// Update dual-language post (updates both EN and HI posts)
router.put('/dual-language/:post_group_id', async (req, res) => {
try {
const {
// English fields
title_en, excerpt_en, content_json_en, external_url_en, status_en,
// Hindi fields
title_hi, excerpt_hi, content_json_hi, external_url_hi, status_hi,
// Shared fields
content_type, thumbnail_url, status
} = req.body
// Support both old format (status) and new format (status_en/status_hi)
const enStatus = status_en !== undefined ? status_en : status
const hiStatus = status_hi !== undefined ? status_hi : status
logger.transaction('UPDATE_DUAL_LANGUAGE_POST', {
postGroupId: req.params.post_group_id,
userId: req.user.id,
status_en: enStatus,
status_hi: hiStatus
})
// Check if posts exist and belong to user
const checkQuery = 'SELECT id, language, thumbnail_url, excerpt, title FROM posts WHERE post_group_id = $1 AND user_id = $2'
logger.db('SELECT', checkQuery, [req.params.post_group_id, req.user.id])
const existingResult = await pool.query(checkQuery, [req.params.post_group_id, req.user.id])
if (existingResult.rows.length === 0) {
logger.warn('POSTS', 'Post group not found for update', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
return res.status(404).json({ message: 'Post group not found' })
}
const existingEn = existingResult.rows.find(p => p.language === 'en')
const existingHi = existingResult.rows.find(p => p.language === 'hi')
const isLinkUpdate = content_type === 'link'
// Validation for publishing English
if (enStatus === 'published') {
const finalTitleEn = title_en !== undefined ? title_en : existingEn?.title
const finalExcerptEn = excerpt_en !== undefined ? excerpt_en : existingEn?.excerpt
if (!finalTitleEn?.trim() || !finalExcerptEn?.trim()) {
return res.status(400).json({ message: 'English title and excerpt required to publish' })
}
if (isLinkUpdate) {
const finalUrlEn = external_url_en !== undefined ? external_url_en : null
if (!finalUrlEn?.trim() || !isValidExternalUrl(finalUrlEn)) {
return res.status(400).json({ message: 'Valid English URL required for link posts' })
}
} else if (content_json_en === undefined && !existingEn?.content_json) {
return res.status(400).json({ message: 'English content required' })
}
const finalThumbnail = thumbnail_url !== undefined ? thumbnail_url : existingEn?.thumbnail_url
if (!finalThumbnail?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Validation for publishing Hindi
if (hiStatus === 'published') {
const finalTitleHi = title_hi !== undefined ? title_hi : existingHi?.title
const finalExcerptHi = excerpt_hi !== undefined ? excerpt_hi : existingHi?.excerpt
if (!finalTitleHi?.trim() || !finalExcerptHi?.trim()) {
return res.status(400).json({ message: 'Hindi title and excerpt required to publish' })
}
if (isLinkUpdate) {
const finalUrlHi = external_url_hi !== undefined ? external_url_hi : null
if (!finalUrlHi?.trim() || !isValidExternalUrl(finalUrlHi)) {
return res.status(400).json({ message: 'Valid Hindi URL required for link posts' })
}
} else if (content_json_hi === undefined && !existingHi?.content_json) {
return res.status(400).json({ message: 'Hindi content required' })
}
const finalThumbnail = thumbnail_url !== undefined ? thumbnail_url : existingHi?.thumbnail_url
if (!finalThumbnail?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Update English post
if (existingEn) {
const updatesEn = []
const valuesEn = []
let paramCount = 1
if (title_en !== undefined) {
updatesEn.push(`title = $${paramCount++}`)
valuesEn.push(title_en || 'Untitled')
// Update slug if title changed
const slugEn = slugify(title_en || 'untitled', { lower: true, strict: true }) + '-' + Date.now()
updatesEn.push(`slug = $${paramCount++}`)
valuesEn.push(slugEn)
}
if (content_json_en !== undefined) {
updatesEn.push(`content_json = $${paramCount++}`)
valuesEn.push(JSON.stringify(content_json_en))
}
if (enStatus !== undefined) {
updatesEn.push(`status = $${paramCount++}`)
valuesEn.push(enStatus)
}
if (content_type !== undefined) {
updatesEn.push(`content_type = $${paramCount++}`)
valuesEn.push(content_type)
}
if (external_url_en !== undefined) {
updatesEn.push(`external_url = $${paramCount++}`)
valuesEn.push(external_url_en?.trim() || null)
}
if (thumbnail_url !== undefined) {
updatesEn.push(`thumbnail_url = $${paramCount++}`)
valuesEn.push(thumbnail_url?.trim() || null)
}
if (excerpt_en !== undefined) {
updatesEn.push(`excerpt = $${paramCount++}`)
valuesEn.push(excerpt_en?.trim()?.slice(0, 500) || null)
}
if (updatesEn.length > 0) {
updatesEn.push(`updated_at = NOW()`)
valuesEn.push(existingEn.id)
const updateQueryEn = `UPDATE posts SET ${updatesEn.join(', ')} WHERE id = $${paramCount} RETURNING *`
logger.db('UPDATE', updateQueryEn, valuesEn)
await pool.query(updateQueryEn, valuesEn)
}
}
// Update Hindi post
if (existingHi) {
const updatesHi = []
const valuesHi = []
let paramCount = 1
if (title_hi !== undefined) {
updatesHi.push(`title = $${paramCount++}`)
valuesHi.push(title_hi || 'शीर्षकहीन')
// Update slug if title changed
const slugHi = slugify(title_hi || 'untitled', { lower: true, strict: true }) + '-' + Date.now() + '-hi'
updatesHi.push(`slug = $${paramCount++}`)
valuesHi.push(slugHi)
}
if (content_json_hi !== undefined) {
updatesHi.push(`content_json = $${paramCount++}`)
valuesHi.push(JSON.stringify(content_json_hi))
}
if (hiStatus !== undefined) {
updatesHi.push(`status = $${paramCount++}`)
valuesHi.push(hiStatus)
}
if (content_type !== undefined) {
updatesHi.push(`content_type = $${paramCount++}`)
valuesHi.push(content_type)
}
if (external_url_hi !== undefined) {
updatesHi.push(`external_url = $${paramCount++}`)
valuesHi.push(external_url_hi?.trim() || null)
}
if (thumbnail_url !== undefined) {
updatesHi.push(`thumbnail_url = $${paramCount++}`)
valuesHi.push(thumbnail_url?.trim() || null)
}
if (excerpt_hi !== undefined) {
updatesHi.push(`excerpt = $${paramCount++}`)
valuesHi.push(excerpt_hi?.trim()?.slice(0, 500) || null)
}
if (updatesHi.length > 0) {
updatesHi.push(`updated_at = NOW()`)
valuesHi.push(existingHi.id)
const updateQueryHi = `UPDATE posts SET ${updatesHi.join(', ')} WHERE id = $${paramCount} RETURNING *`
logger.db('UPDATE', updateQueryHi, valuesHi)
await pool.query(updateQueryHi, valuesHi)
}
}
// Fetch updated posts
const result = await pool.query('SELECT * FROM posts WHERE post_group_id = $1 AND user_id = $2 ORDER BY language',
[req.params.post_group_id, req.user.id])
logger.transaction('UPDATE_DUAL_LANGUAGE_POST_SUCCESS', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
res.json({
post_group_id: req.params.post_group_id,
english: result.rows.find(p => p.language === 'en') || null,
hindi: result.rows.find(p => p.language === 'hi') || null
})
} catch (error) {
logger.error('POSTS', 'Error updating dual-language post', error)
res.status(500).json({ message: 'Failed to update posts', error: error.message })
}
})
export default router

View File

@ -17,7 +17,17 @@ export default function Dashboard() {
const fetchPosts = async () => {
try {
const res = await api.get('/posts')
setPosts(res.data)
// Group posts by post_group_id
const grouped = {}
res.data.forEach(post => {
const groupId = post.post_group_id || post.id
if (!grouped[groupId]) {
grouped[groupId] = { en: null, hi: null, groupId }
}
if (post.language === 'en') grouped[groupId].en = post
if (post.language === 'hi') grouped[groupId].hi = post
})
setPosts(Object.values(grouped))
} catch (error) {
toast.error('Failed to load posts')
} finally {
@ -25,25 +35,51 @@ export default function Dashboard() {
}
}
const handleDelete = async (id) => {
if (!window.confirm('Are you sure you want to delete this post?')) return
const handleDelete = async (groupId, enId, hiId) => {
if (!window.confirm('Are you sure you want to delete both language versions of this post?')) return
try {
await api.delete(`/posts/${id}`)
toast.success('Post deleted')
// Delete both English and Hindi versions
if (enId) await api.delete(`/posts/${enId}`)
if (hiId) await api.delete(`/posts/${hiId}`)
toast.success('Posts deleted')
fetchPosts()
} catch (error) {
toast.error('Failed to delete post')
toast.error('Failed to delete posts')
}
}
const handlePublish = async (post) => {
const handlePublish = async (postGroup) => {
const post = postGroup.en || postGroup.hi
if (!post) return
const newStatus = post.status === 'published' ? 'draft' : 'published'
try {
await api.put(`/posts/${post.id}`, {
...post,
status: post.status === 'published' ? 'draft' : 'published'
})
toast.success(`Post ${post.status === 'published' ? 'unpublished' : 'published'}`)
// Update both posts via dual-language endpoint if they exist
if (postGroup.en && postGroup.hi) {
await api.put(`/posts/dual-language/${postGroup.groupId}`, {
title_en: postGroup.en.title,
title_hi: postGroup.hi.title,
excerpt_en: postGroup.en.excerpt,
excerpt_hi: postGroup.hi.excerpt,
content_json_en: postGroup.en.content_json,
content_json_hi: postGroup.hi.content_json,
external_url_en: postGroup.en.external_url,
external_url_hi: postGroup.hi.external_url,
content_type: post.content_type,
thumbnail_url: post.thumbnail_url,
status: newStatus
})
} else {
// Fallback for single language posts (legacy)
const singlePost = postGroup.en || postGroup.hi
await api.put(`/posts/${singlePost.id}`, {
...singlePost,
status: newStatus
})
}
toast.success(`Post ${newStatus === 'published' ? 'published' : 'unpublished'}`)
fetchPosts()
} catch (error) {
toast.error('Failed to update post')
@ -98,74 +134,145 @@ export default function Dashboard() {
</div>
) : (
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<div key={post.id} className="bg-white rounded-lg shadow p-4 sm:p-6">
<div className="flex justify-between items-start gap-2 mb-2">
<h3 className="text-base sm:text-lg font-semibold text-gray-900 line-clamp-2 min-w-0">
{post.title || 'Untitled'}
</h3>
<div className="flex flex-shrink-0 items-center gap-1.5 flex-wrap justify-end">
{post.content_type === 'link' && (
<span className="px-2 py-1 text-xs rounded bg-blue-100 text-blue-800">
Link
{posts.map((postGroup) => {
const post = postGroup.en || postGroup.hi
if (!post) return null
return (
<div key={postGroup.groupId} className="bg-white rounded-lg shadow p-4 sm:p-6">
{/* Display both language titles */}
<div className="mb-3 space-y-2">
<div className="flex items-start gap-2">
<span className={`px-2 py-1 text-xs rounded font-medium flex-shrink-0 ${
postGroup.en?.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-indigo-100 text-indigo-800'
}`}>
EN
</span>
)}
<span
className={`px-2 py-1 text-xs rounded ${
post.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}
<h3 className="text-sm font-semibold text-gray-900 line-clamp-2 flex-1">
{postGroup.en?.title || 'No English version'}
</h3>
</div>
<div className="flex items-start gap-2">
<span className={`px-2 py-1 text-xs rounded font-medium flex-shrink-0 ${
postGroup.hi?.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-amber-100 text-amber-800'
}`}>
HI
</span>
<h3 className="text-sm font-semibold text-gray-900 line-clamp-2 flex-1">
{postGroup.hi?.title || 'No Hindi version'}
</h3>
</div>
</div>
{/* Status badges */}
<div className="space-y-2 mb-4">
<div className="flex flex-wrap items-center gap-2">
{post.content_type === 'link' && (
<span className="px-2 py-1 text-xs rounded bg-blue-100 text-blue-800 font-medium">
Link
</span>
)}
{/* Show publish status for each language */}
<div className="flex items-center gap-1.5">
{postGroup.en && (
<span
className={`px-2 py-1 text-xs rounded font-medium ${
postGroup.en.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-600'
}`}
title={`English: ${postGroup.en.status}`}
>
EN: {postGroup.en.status === 'published' ? '✓' : '○'}
</span>
)}
{postGroup.hi && (
<span
className={`px-2 py-1 text-xs rounded font-medium ${
postGroup.hi.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-600'
}`}
title={`Hindi: ${postGroup.hi.status}`}
>
HI: {postGroup.hi.status === 'published' ? '✓' : '○'}
</span>
)}
</div>
<span className="text-xs text-gray-500 ml-auto">
{new Date(post.updated_at).toLocaleDateString()}
</span>
</div>
{/* Summary status message */}
<div className="text-xs font-medium">
{(() => {
const enPublished = postGroup.en?.status === 'published'
const hiPublished = postGroup.hi?.status === 'published'
if (enPublished && hiPublished) {
return <span className="text-green-700">📱 Published in: <strong>Both Languages</strong></span>
} else if (enPublished) {
return <span className="text-indigo-700">📱 Published in: <strong>English Only</strong></span>
} else if (hiPublished) {
return <span className="text-amber-700">📱 Published in: <strong>Hindi Only</strong></span>
} else {
return <span className="text-gray-600">📝 Draft - Not Published</span>
}
})()}
</div>
</div>
{/* Action buttons */}
<div className="flex flex-wrap gap-2">
<Link
to={`/editor/${postGroup.groupId}`}
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 text-center bg-indigo-600 text-white px-3 py-2 rounded-md hover:bg-indigo-700 text-sm font-medium"
>
{post.status}
</span>
Edit
</Link>
{post.status === 'published' && postGroup.en && (
post.content_type === 'link' && postGroup.en.external_url ? (
<a
href={postGroup.en.external_url}
target="_blank"
rel="noopener noreferrer"
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 text-center bg-gray-200 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-300 text-sm"
>
View EN
</a>
) : (
<Link
to={`/blog/${postGroup.en.slug}`}
target="_blank"
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 text-center bg-gray-200 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-300 text-sm"
>
View EN
</Link>
)
)}
<button
onClick={() => handlePublish(postGroup)}
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 px-3 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 text-sm"
>
{post.status === 'published' ? 'Unpublish' : 'Publish'}
</button>
<button
onClick={() => handleDelete(postGroup.groupId, postGroup.en?.id, postGroup.hi?.id)}
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm"
>
Delete
</button>
</div>
</div>
<p className="text-sm text-gray-500 mb-4">
{new Date(post.updated_at).toLocaleDateString()}
</p>
<div className="flex flex-wrap gap-2">
<Link
to={`/editor/${post.id}`}
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 text-center bg-indigo-600 text-white px-3 py-2 rounded-md hover:bg-indigo-700 text-sm"
>
Edit
</Link>
{post.status === 'published' && (
post.content_type === 'link' && post.external_url ? (
<a
href={post.external_url}
target="_blank"
rel="noopener noreferrer"
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 text-center bg-gray-200 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-300 text-sm"
>
View
</a>
) : (
<Link
to={`/blog/${post.slug}`}
target="_blank"
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 text-center bg-gray-200 text-gray-700 px-3 py-2 rounded-md hover:bg-gray-300 text-sm"
>
View
</Link>
)
)}
<button
onClick={() => handlePublish(post)}
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 px-3 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 text-sm"
>
{post.status === 'published' ? 'Unpublish' : 'Publish'}
</button>
<button
onClick={() => handleDelete(post.id)}
className="flex-1 min-w-[calc(50%-4px)] sm:min-w-0 px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm"
>
Delete
</button>
</div>
</div>
))}
)
})}
</div>
)}
</div>

File diff suppressed because it is too large Load Diff