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) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
logger.transaction('FETCH_POSTS', { userId: req.user.id })
|
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])
|
logger.db('SELECT', query, [req.user.id])
|
||||||
|
|
||||||
const result = await pool.query(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
|
// Create post
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
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', {
|
logger.transaction('CREATE_POST', {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
title: title?.substring(0, 50),
|
title: title?.substring(0, 50),
|
||||||
status: status || 'draft',
|
status: status || 'draft',
|
||||||
content_type: content_type || 'tiptap'
|
content_type: content_type || 'tiptap',
|
||||||
|
language: language || 'en'
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLinkPost = content_type === 'link'
|
const isLinkPost = content_type === 'link'
|
||||||
|
|
@ -127,6 +128,7 @@ router.post('/', async (req, res) => {
|
||||||
const contentType = isLinkPost ? 'link' : (content_type || 'tiptap')
|
const contentType = isLinkPost ? 'link' : (content_type || 'tiptap')
|
||||||
const contentJson = isLinkPost ? {} : content_json
|
const contentJson = isLinkPost ? {} : content_json
|
||||||
const externalUrl = isLinkPost ? external_url.trim() : null
|
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 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
|
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)
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING *`
|
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, [
|
const result = await pool.query(query, [
|
||||||
req.user.id,
|
req.user.id,
|
||||||
|
|
@ -154,13 +156,15 @@ router.post('/', async (req, res) => {
|
||||||
contentType,
|
contentType,
|
||||||
externalUrl,
|
externalUrl,
|
||||||
thumbnailUrl,
|
thumbnailUrl,
|
||||||
excerptVal
|
excerptVal,
|
||||||
|
postLanguage
|
||||||
])
|
])
|
||||||
|
|
||||||
logger.transaction('CREATE_POST_SUCCESS', {
|
logger.transaction('CREATE_POST_SUCCESS', {
|
||||||
postId: result.rows[0].id,
|
postId: result.rows[0].id,
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
slug: result.rows[0].slug
|
slug: result.rows[0].slug,
|
||||||
|
language: postLanguage
|
||||||
})
|
})
|
||||||
res.status(201).json(result.rows[0])
|
res.status(201).json(result.rows[0])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -172,7 +176,7 @@ router.post('/', async (req, res) => {
|
||||||
// Update post
|
// Update post
|
||||||
router.put('/:id', async (req, res) => {
|
router.put('/:id', async (req, res) => {
|
||||||
try {
|
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', {
|
logger.transaction('UPDATE_POST', {
|
||||||
postId: req.params.id,
|
postId: req.params.id,
|
||||||
|
|
@ -184,7 +188,8 @@ router.put('/:id', async (req, res) => {
|
||||||
content_type: content_type !== undefined,
|
content_type: content_type !== undefined,
|
||||||
external_url: external_url !== undefined,
|
external_url: external_url !== undefined,
|
||||||
thumbnail_url: thumbnail_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)
|
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()`)
|
updates.push(`updated_at = NOW()`)
|
||||||
values.push(req.params.id, req.user.id)
|
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
|
export default router
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,17 @@ export default function Dashboard() {
|
||||||
const fetchPosts = async () => {
|
const fetchPosts = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/posts')
|
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) {
|
} catch (error) {
|
||||||
toast.error('Failed to load posts')
|
toast.error('Failed to load posts')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -25,25 +35,51 @@ export default function Dashboard() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (groupId, enId, hiId) => {
|
||||||
if (!window.confirm('Are you sure you want to delete this post?')) return
|
if (!window.confirm('Are you sure you want to delete both language versions of this post?')) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.delete(`/posts/${id}`)
|
// Delete both English and Hindi versions
|
||||||
toast.success('Post deleted')
|
if (enId) await api.delete(`/posts/${enId}`)
|
||||||
|
if (hiId) await api.delete(`/posts/${hiId}`)
|
||||||
|
toast.success('Posts deleted')
|
||||||
fetchPosts()
|
fetchPosts()
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
await api.put(`/posts/${post.id}`, {
|
// Update both posts via dual-language endpoint if they exist
|
||||||
...post,
|
if (postGroup.en && postGroup.hi) {
|
||||||
status: post.status === 'published' ? 'draft' : 'published'
|
await api.put(`/posts/dual-language/${postGroup.groupId}`, {
|
||||||
})
|
title_en: postGroup.en.title,
|
||||||
toast.success(`Post ${post.status === 'published' ? 'unpublished' : 'published'}`)
|
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()
|
fetchPosts()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update post')
|
toast.error('Failed to update post')
|
||||||
|
|
@ -98,74 +134,145 @@ export default function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{posts.map((post) => (
|
{posts.map((postGroup) => {
|
||||||
<div key={post.id} className="bg-white rounded-lg shadow p-4 sm:p-6">
|
const post = postGroup.en || postGroup.hi
|
||||||
<div className="flex justify-between items-start gap-2 mb-2">
|
if (!post) return null
|
||||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900 line-clamp-2 min-w-0">
|
|
||||||
{post.title || 'Untitled'}
|
return (
|
||||||
</h3>
|
<div key={postGroup.groupId} className="bg-white rounded-lg shadow p-4 sm:p-6">
|
||||||
<div className="flex flex-shrink-0 items-center gap-1.5 flex-wrap justify-end">
|
{/* Display both language titles */}
|
||||||
{post.content_type === 'link' && (
|
<div className="mb-3 space-y-2">
|
||||||
<span className="px-2 py-1 text-xs rounded bg-blue-100 text-blue-800">
|
<div className="flex items-start gap-2">
|
||||||
Link
|
<span className={`px-2 py-1 text-xs rounded font-medium flex-shrink-0 ${
|
||||||
</span>
|
postGroup.en?.status === 'published'
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 text-xs rounded ${
|
|
||||||
post.status === 'published'
|
|
||||||
? 'bg-green-100 text-green-800'
|
? 'bg-green-100 text-green-800'
|
||||||
: 'bg-gray-100 text-gray-800'
|
: 'bg-indigo-100 text-indigo-800'
|
||||||
}`}
|
}`}>
|
||||||
|
EN
|
||||||
|
</span>
|
||||||
|
<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}
|
Edit
|
||||||
</span>
|
</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>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue