updated for languagees
This commit is contained in:
parent
cb8b768d3c
commit
26a617e1d4
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue