import { useState, useEffect, useRef, useCallback } from 'react' import { useParams, useNavigate } from 'react-router-dom' import Editor from '../components/Editor' import MobilePreview from '../components/MobilePreview' import api from '../utils/api' import toast from 'react-hot-toast' export default function EditorPage() { const { id } = useParams() const navigate = useNavigate() // Publish mode - what to publish when user clicks publish const [publishMode, setPublishMode] = useState('both') // 'en', 'hi', or 'both' // Dual-language state const [titleEn, setTitleEn] = useState('') const [titleHi, setTitleHi] = useState('') const [excerptEn, setExcerptEn] = useState('') const [excerptHi, setExcerptHi] = useState('') const [contentEn, setContentEn] = useState(null) const [contentHi, setContentHi] = useState(null) const [externalUrlEn, setExternalUrlEn] = useState('') const [externalUrlHi, setExternalUrlHi] = useState('') const [postGroupId, setPostGroupId] = useState(null) const [singlePostId, setSinglePostId] = useState(null) // For single-language posts // Shared state const [contentType, setContentType] = useState('tiptap') // 'tiptap' | 'link' const [thumbnailUrl, setThumbnailUrl] = useState('') const [createdAt, setCreatedAt] = useState(null) const [loading, setLoading] = useState(!!id) const [saving, setSaving] = useState(false) const [uploadingThumbnail, setUploadingThumbnail] = useState(false) const thumbnailInputRef = useRef(null) const [showPreview, setShowPreview] = useState(false) const autoSaveTimeoutRef = useRef(null) const isInitialLoadRef = useRef(true) const currentPostGroupIdRef = useRef(id) const sessionIdRef = useRef(null) if (!id && !sessionIdRef.current) { sessionIdRef.current = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `draft-${Date.now()}-${Math.random().toString(36).slice(2)}` } useEffect(() => { currentPostGroupIdRef.current = id if (id) { fetchPost() } else { setLoading(false) } }, [id]) // Build dual-language post payload const buildDualLanguageData = useCallback((options = {}) => { const { status_en, status_hi, ...overrides } = options const isLink = contentType === 'link' const data = { title_en: titleEn?.trim() || 'Untitled', title_hi: titleHi?.trim() || 'शीर्षकहीन', excerpt_en: excerptEn?.trim() || null, excerpt_hi: excerptHi?.trim() || null, content_json_en: isLink ? {} : (contentEn || {}), content_json_hi: isLink ? {} : (contentHi || {}), external_url_en: isLink ? externalUrlEn?.trim() : null, external_url_hi: isLink ? externalUrlHi?.trim() : null, content_type: contentType, thumbnail_url: thumbnailUrl?.trim() || null, ...overrides } // Only include status fields if explicitly provided if (status_en !== undefined) data.status_en = status_en if (status_hi !== undefined) data.status_hi = status_hi return data }, [titleEn, titleHi, excerptEn, excerptHi, contentEn, contentHi, externalUrlEn, externalUrlHi, contentType, thumbnailUrl]) // Fetch post (handles both single-language and dual-language) const fetchPost = async () => { try { // First try to fetch as post group (dual-language) let isGroupPost = false let data = null try { const res = await api.get(`/posts/group/${id}`) data = res.data isGroupPost = true } catch (groupError) { // Not a group post, will try single post isGroupPost = false } if (isGroupPost && data) { // Dual-language post setPostGroupId(data.post_group_id) // Load English version if (data.english) { setTitleEn(data.english.title || '') setExcerptEn(data.english.excerpt || '') setContentEn(data.english.content_json || null) setExternalUrlEn(data.english.external_url || '') setCreatedAt(data.english.created_at || null) } // Load Hindi version if (data.hindi) { setTitleHi(data.hindi.title || '') setExcerptHi(data.hindi.excerpt || '') setContentHi(data.hindi.content_json || null) setExternalUrlHi(data.hindi.external_url || '') } // Load shared fields const post = data.english || data.hindi if (post) { setContentType(post.content_type === 'link' ? 'link' : 'tiptap') setThumbnailUrl(post.thumbnail_url || '') } // Set publish mode based on which language(s) are PUBLISHED const enPublished = data.english?.status === 'published' const hiPublished = data.hindi?.status === 'published' if (enPublished && hiPublished) { setPublishMode('both') } else if (enPublished) { setPublishMode('en') } else if (hiPublished) { setPublishMode('hi') } else { // Neither published, default based on what data exists if (data.english && data.hindi) { setPublishMode('both') } else if (data.english) { setPublishMode('en') } else { setPublishMode('hi') } } } else { // Single-language post - try fetching by post ID const res = await api.get(`/posts/${id}`) const post = res.data setSinglePostId(post.id) const lang = post.language || 'en' // Load into appropriate language state if (lang === 'en') { setTitleEn(post.title || '') setExcerptEn(post.excerpt || '') setContentEn(post.content_json || null) setExternalUrlEn(post.external_url || '') setPublishMode('en') } else { setTitleHi(post.title || '') setExcerptHi(post.excerpt || '') setContentHi(post.content_json || null) setExternalUrlHi(post.external_url || '') setPublishMode('hi') } setContentType(post.content_type === 'link' ? 'link' : 'tiptap') setThumbnailUrl(post.thumbnail_url || '') setCreatedAt(post.created_at || null) } isInitialLoadRef.current = true } catch (error) { console.error('Error loading post:', error) toast.error('Failed to load post: ' + (error.response?.data?.message || error.message)) navigate('/dashboard') } finally { setLoading(false) setTimeout(() => { isInitialLoadRef.current = false }, 500) } } // Debounced auto-save function - always saves all available data const handleAutoSave = useCallback(async () => { if (isInitialLoadRef.current) return const hasEnglishData = titleEn?.trim() || excerptEn?.trim() || (contentType === 'link' ? externalUrlEn?.trim() : contentEn) const hasHindiData = titleHi?.trim() || excerptHi?.trim() || (contentType === 'link' ? externalUrlHi?.trim() : contentHi) if (!hasEnglishData && !hasHindiData) return try { setSaving(true) // If we have data for both languages, use dual-language endpoint if (hasEnglishData && hasHindiData) { const postData = buildDualLanguageData({ status_en: 'draft', status_hi: 'draft' }) let groupId = currentPostGroupIdRef.current || postGroupId if (groupId) { await api.put(`/posts/dual-language/${groupId}`, postData) } else { const res = await api.post('/posts/dual-language', postData) groupId = res.data.post_group_id currentPostGroupIdRef.current = groupId setPostGroupId(groupId) window.history.replaceState({}, '', `/editor/${groupId}`) } } else { // Only one language has data, use single-language endpoint const lang = hasEnglishData ? 'en' : 'hi' const postData = { title: lang === 'en' ? (titleEn?.trim() || 'Untitled') : (titleHi?.trim() || 'शीर्षकहीन'), excerpt: lang === 'en' ? (excerptEn?.trim() || null) : (excerptHi?.trim() || null), content_json: contentType === 'link' ? {} : (lang === 'en' ? (contentEn || {}) : (contentHi || {})), external_url: contentType === 'link' ? (lang === 'en' ? externalUrlEn?.trim() : externalUrlHi?.trim()) : null, content_type: contentType, thumbnail_url: thumbnailUrl?.trim() || null, language: lang, status: 'draft' } if (singlePostId) { await api.put(`/posts/${singlePostId}`, postData) } else { const res = await api.post('/posts', postData) setSinglePostId(res.data.id) window.history.replaceState({}, '', `/editor/${res.data.id}`) } } } catch (error) { console.error('Auto-save failed:', error) } finally { setSaving(false) } }, [titleEn, titleHi, excerptEn, excerptHi, contentEn, contentHi, externalUrlEn, externalUrlHi, contentType, thumbnailUrl, postGroupId, singlePostId, buildDualLanguageData]) // Debounced save on content change useEffect(() => { if (isInitialLoadRef.current) { isInitialLoadRef.current = false return } if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current) } autoSaveTimeoutRef.current = setTimeout(() => { handleAutoSave() }, 2000) return () => { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current) } } }, [titleEn, titleHi, excerptEn, excerptHi, contentEn, contentHi, externalUrlEn, externalUrlHi, contentType, thumbnailUrl, handleAutoSave]) const postIdForThumbnail = singlePostId || id || currentPostGroupIdRef.current || postGroupId const handleThumbnailUpload = async (file) => { if (!file?.type?.startsWith('image/')) { toast.error('Please select an image file') return } if (file.size > 5 * 1024 * 1024) { toast.error('Image must be under 5MB') return } if (!postIdForThumbnail) { toast.error('Save draft first to add a thumbnail') return } try { setUploadingThumbnail(true) toast.loading('Uploading thumbnail...', { id: 'thumbnail' }) const res = await api.post('/upload/presigned-url', { filename: file.name, contentType: file.type, postId: postIdForThumbnail, purpose: 'thumbnail', }) const { uploadUrl, imageUrl } = res.data const putRes = await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type }, }) if (!putRes.ok) { throw new Error('Upload failed') } setThumbnailUrl(imageUrl) // Update posts with new thumbnail if (postIdForThumbnail) { // Determine which endpoint to use based on what exists if (postGroupId) { const postData = buildDualLanguageData({ thumbnail_url: imageUrl }) await api.put(`/posts/dual-language/${postGroupId}`, postData) } else if (singlePostId) { // Check if we have both languages now const hasEnglishData = titleEn?.trim() || excerptEn?.trim() const hasHindiData = titleHi?.trim() || excerptHi?.trim() if (hasEnglishData && hasHindiData) { const postData = buildDualLanguageData({ thumbnail_url: imageUrl, status_en: 'draft', status_hi: 'draft' }) const res = await api.post('/posts/dual-language', postData) setPostGroupId(res.data.post_group_id) setSinglePostId(null) } else { const lang = hasEnglishData ? 'en' : 'hi' const postData = { title: lang === 'en' ? titleEn?.trim() : titleHi?.trim(), excerpt: lang === 'en' ? excerptEn?.trim() : excerptHi?.trim(), content_json: contentType === 'link' ? {} : (lang === 'en' ? (contentEn || {}) : (contentHi || {})), external_url: contentType === 'link' ? (lang === 'en' ? externalUrlEn?.trim() : externalUrlHi?.trim()) : null, content_type: contentType, thumbnail_url: imageUrl, language: lang } await api.put(`/posts/${singlePostId}`, postData) } } } toast.success('Thumbnail saved', { id: 'thumbnail' }) } catch (err) { toast.error(err.response?.data?.message || err.message || 'Failed to upload thumbnail', { id: 'thumbnail' }) } finally { setUploadingThumbnail(false) } } const handleRemoveThumbnail = () => { setThumbnailUrl('') if (postIdForThumbnail) { if (postGroupId) { const postData = buildDualLanguageData({ thumbnail_url: null }) api.put(`/posts/dual-language/${postGroupId}`, postData).catch(() => {}) } else if (singlePostId) { const hasEnglishData = titleEn?.trim() || excerptEn?.trim() const hasHindiData = titleHi?.trim() || excerptHi?.trim() if (hasEnglishData && hasHindiData) { const postData = buildDualLanguageData({ thumbnail_url: null }) api.put(`/posts/dual-language/${postGroupId}`, postData).catch(() => {}) } else { const lang = hasEnglishData ? 'en' : 'hi' const postData = { title: lang === 'en' ? titleEn?.trim() : titleHi?.trim(), excerpt: lang === 'en' ? excerptEn?.trim() : excerptHi?.trim(), content_json: contentType === 'link' ? {} : (lang === 'en' ? (contentEn || {}) : (contentHi || {})), external_url: contentType === 'link' ? (lang === 'en' ? externalUrlEn?.trim() : externalUrlHi?.trim()) : null, content_type: contentType, thumbnail_url: null, language: lang } api.put(`/posts/${singlePostId}`, postData).catch(() => {}) } } } } const handlePublish = async () => { if (!thumbnailUrl.trim()) { toast.error('Thumbnail is required to publish') return } // Validate based on publish mode if (publishMode === 'en' || publishMode === 'both') { // Validate English if (!titleEn.trim()) { toast.error('English title is required') return } if (!excerptEn.trim()) { toast.error('English excerpt is required') return } if (contentType === 'link' && !externalUrlEn.trim()) { toast.error('English URL is required for link posts') return } if (contentType === 'link') { const url = externalUrlEn.trim() if (!url.startsWith('http://') && !url.startsWith('https://')) { toast.error('English URL must start with http:// or https://') return } } if (contentType !== 'link' && !contentEn) { toast.error('English content is required') return } } if (publishMode === 'hi' || publishMode === 'both') { // Validate Hindi if (!titleHi.trim()) { toast.error('Hindi title is required') return } if (!excerptHi.trim()) { toast.error('Hindi excerpt is required') return } if (contentType === 'link' && !externalUrlHi.trim()) { toast.error('Hindi URL is required for link posts') return } if (contentType === 'link') { const url = externalUrlHi.trim() if (!url.startsWith('http://') && !url.startsWith('https://')) { toast.error('Hindi URL must start with http:// or https://') return } } if (contentType !== 'link' && !contentHi) { toast.error('Hindi content is required') return } } try { setSaving(true) // Check if we have content in both languages const hasEnglish = titleEn?.trim() || excerptEn?.trim() || contentEn || externalUrlEn?.trim() const hasHindi = titleHi?.trim() || excerptHi?.trim() || contentHi || externalUrlHi?.trim() // If we have both languages OR already have a postGroupId, use dual-language endpoint if ((hasEnglish && hasHindi) || postGroupId) { // Determine status for each language based on publishMode let enStatus = 'draft' let hiStatus = 'draft' if (publishMode === 'both') { enStatus = 'published' hiStatus = 'published' } else if (publishMode === 'en') { enStatus = 'published' hiStatus = 'draft' } else if (publishMode === 'hi') { enStatus = 'draft' hiStatus = 'published' } // Build data with appropriate statuses const postData = buildDualLanguageData({ status_en: enStatus, status_hi: hiStatus }) // Only use PUT if we have a REAL post_group_id (not a single post ID) // The postGroupId state is only set when we have actual linked posts const actualGroupId = postGroupId || currentPostGroupIdRef.current if (actualGroupId && !singlePostId) { // We have a real post group ID - update it await api.put(`/posts/dual-language/${actualGroupId}`, postData) } else { // Creating new dual-language post or converting from single-language const res = await api.post('/posts/dual-language', postData) const newGroupId = res.data.post_group_id setPostGroupId(newGroupId) currentPostGroupIdRef.current = newGroupId setSinglePostId(null) // Clear single post ID since we now have a group window.history.replaceState({}, '', `/editor/${newGroupId}`) } if (publishMode === 'both') { toast.success('Posts published in both languages!') } else { toast.success(`${publishMode === 'en' ? 'English' : 'Hindi'} published! Other language saved as draft.`) } } else { // Single language only, no dual-language post exists const lang = publishMode const postData = { title: lang === 'en' ? titleEn?.trim() : titleHi?.trim(), excerpt: lang === 'en' ? excerptEn?.trim() : excerptHi?.trim(), content_json: contentType === 'link' ? {} : (lang === 'en' ? (contentEn || {}) : (contentHi || {})), external_url: contentType === 'link' ? (lang === 'en' ? externalUrlEn?.trim() : externalUrlHi?.trim()) : null, content_type: contentType, thumbnail_url: thumbnailUrl?.trim() || null, language: lang, status: 'published' } if (singlePostId) { await api.put(`/posts/${singlePostId}`, postData) } else { const res = await api.post('/posts', postData) setSinglePostId(res.data.id) } toast.success(`Post published in ${lang === 'en' ? 'English' : 'Hindi'}!`) } // Don't navigate immediately - let user see the success message setTimeout(() => navigate('/dashboard'), 1000) } catch (error) { toast.error(error.response?.data?.message || 'Failed to publish post') } finally { setSaving(false) } } // Cleanup on unmount useEffect(() => { return () => { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current) } } }, []) if (loading) { return
Loading...
} return (
{/* Top bar */}
{saving && ( Saving... )}
{/* Main Editor Section */}
{/* Publish mode selector and Post type selector */}
{/* Publish Mode - What to publish */}
{/* Post type selector */}
{/* Shared thumbnail section */}
{!postIdForThumbnail ? (

Save draft first to add a thumbnail. Required for publishing.

) : (
{thumbnailUrl ? (
Thumbnail
) : null}
{ const f = e.target.files?.[0] if (f) handleThumbnailUpload(f) e.target.value = '' }} />

Square image recommended. Required for publishing.

)}
{/* Always show dual-language layout - all content preserved */}
{/* English Section */}

English

{(publishMode === 'en' || publishMode === 'both') && ( Will Publish )}
setTitleEn(e.target.value)} className="w-full text-xl font-bold p-2 border-0 border-b-2 border-transparent hover:border-gray-200 focus:outline-none focus:border-indigo-500 bg-transparent transition-colors mb-4" />