From ba3900bc33de5cfa2c15601d862034299e295c75 Mon Sep 17 00:00:00 2001 From: fahed Date: Wed, 11 Mar 2026 17:27:57 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20simplify=20translations=20=E2=80=94?= =?UTF-8?q?=20shared=20utils,=20deduplicated=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract shared constants to client/src/utils/translations.js (AVAILABLE_LANGUAGES, TRANSLATION_STATUS_COLORS, isTextSelected, groupTextsByLanguage) - TranslationDetailPanel: deduplicate copy button JSX, hoist hasSelected - PublicTranslationReview: memoize textsByLanguage, use shared isTextSelected - Translations page: import from shared module - Server: translation schema updates, post_id linking - Add reassign-user utility script - Add new translation i18n keys to en.json and ar.json Co-Authored-By: Claude Opus 4.6 --- .../src/components/TranslationDetailPanel.jsx | 292 +++++++++++------ client/src/i18n/ar.json | 50 ++- client/src/i18n/en.json | 52 ++- client/src/pages/PublicTranslationReview.jsx | 308 ++++++++++++++++-- client/src/pages/Translations.jsx | 110 +++++-- client/src/utils/translations.js | 30 ++ server/reassign-user.js | 37 +++ server/server.js | 140 ++++++-- 8 files changed, 826 insertions(+), 193 deletions(-) create mode 100644 client/src/utils/translations.js create mode 100644 server/reassign-user.js diff --git a/client/src/components/TranslationDetailPanel.jsx b/client/src/components/TranslationDetailPanel.jsx index 39a9ea3..8740271 100644 --- a/client/src/components/TranslationDetailPanel.jsx +++ b/client/src/components/TranslationDetailPanel.jsx @@ -1,35 +1,22 @@ import { useState, useEffect, useContext } from 'react' -import { Plus, Copy, Check, ExternalLink, Trash2, Save, FileEdit, Languages, ShieldCheck, Globe } from 'lucide-react' +import { Plus, Copy, Check, ExternalLink, Trash2, Save, FileEdit, Languages, ShieldCheck, Globe, Lock } from 'lucide-react' import { AppContext } from '../App' import { useLanguage } from '../i18n/LanguageContext' import { api } from '../utils/api' +import { AVAILABLE_LANGUAGES, TRANSLATION_STATUS_COLORS, isTextSelected, groupTextsByLanguage } from '../utils/translations' import Modal from './Modal' import TabbedModal from './TabbedModal' import { useToast } from './ToastContainer' import ApproverMultiSelect from './ApproverMultiSelect' -const STATUS_COLORS = { - draft: 'bg-surface-tertiary text-text-secondary', - pending_review: 'bg-amber-100 text-amber-700', - approved: 'bg-emerald-100 text-emerald-700', - rejected: 'bg-red-100 text-red-700', - revision_requested: 'bg-orange-100 text-orange-700', -} - -const AVAILABLE_LANGUAGES = [ - { code: 'AR', label: 'العربية' }, - { code: 'EN', label: 'English' }, - { code: 'FR', label: 'Français' }, - { code: 'ID', label: 'Bahasa Indonesia' }, -] - -export default function TranslationDetailPanel({ translation, onClose, onUpdate, onDelete, assignableUsers = [] }) { +export default function TranslationDetailPanel({ translation, onClose, onUpdate, onDelete, assignableUsers = [], posts: externalPosts }) { const { t } = useLanguage() const { brands } = useContext(AppContext) const toast = useToast() + const isApproved = translation.status === 'approved' + const [editTitle, setEditTitle] = useState(translation.title || '') - const [editDescription, setEditDescription] = useState(translation.description || '') const [editSourceContent, setEditSourceContent] = useState(translation.source_content || '') const [editSourceLanguage, setEditSourceLanguage] = useState(translation.source_language || 'EN') const [editApproverIds, setEditApproverIds] = useState( @@ -44,6 +31,13 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, const [submitting, setSubmitting] = useState(false) const [copied, setCopied] = useState(false) const [freshReviewUrl, setFreshReviewUrl] = useState('') + const [copiedTextId, setCopiedTextId] = useState(null) + + // Post selector + const [posts, setPosts] = useState(externalPosts || []) + const [showCreatePost, setShowCreatePost] = useState(false) + const [newPostTitle, setNewPostTitle] = useState('') + const [creatingPost, setCreatingPost] = useState(false) // Language add modal const [showAddLang, setShowAddLang] = useState(false) @@ -62,9 +56,12 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, loadTexts() }, [translation.Id]) + useEffect(() => { + if (externalPosts) setPosts(externalPosts) + }, [externalPosts]) + useEffect(() => { setEditTitle(translation.title || '') - setEditDescription(translation.description || '') setEditSourceContent(translation.source_content || '') setEditSourceLanguage(translation.source_language || 'EN') setEditApproverIds( @@ -92,7 +89,6 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, try { await api.patch(`/translations/${translation.Id}`, { title: editTitle, - description: editDescription, source_content: editSourceContent, source_language: editSourceLanguage, approver_ids: editApproverIds.length > 0 ? editApproverIds.join(',') : null, @@ -142,11 +138,7 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, const handleUpdateText = async (textId) => { try { - const text = texts.find(t => t.Id === textId) - if (!text) return - await api.post(`/translations/${translation.Id}/texts`, { - language_code: text.language_code, - language_label: text.language_label, + await api.patch(`/translations/${translation.Id}/texts/${textId}`, { content: editingContent, }) toast.success(t('translations.updated')) @@ -197,9 +189,35 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, } } - // Available languages for adding (exclude source + already added) - const usedCodes = new Set([translation.source_language, ...texts.map(t => t.language_code)]) - const availableForAdd = AVAILABLE_LANGUAGES.filter(l => !usedCodes.has(l.code)) + const handleCreatePost = async () => { + if (!newPostTitle.trim()) return + setCreatingPost(true) + try { + const created = await api.post('/posts', { title: newPostTitle, status: 'draft' }) + const postId = created.Id || created.id || created._id + setPosts(prev => [created, ...prev]) + await handleFieldUpdate('post_id', postId) + setShowCreatePost(false) + setNewPostTitle('') + } catch (err) { + toast.error(t('translations.postCreateFailed')) + } finally { + setCreatingPost(false) + } + } + + const copyTextContent = (content, id) => { + navigator.clipboard.writeText(content) + setCopiedTextId(id) + toast.success(t('translations.copiedToClipboard')) + setTimeout(() => setCopiedTextId(null), 2000) + } + + // Available languages (exclude source language only — multiple options per language allowed) + const targetLanguages = AVAILABLE_LANGUAGES.filter(l => l.code !== translation.source_language) + + // Group texts by language + const textsByLanguage = groupTextsByLanguage(texts) const tabs = [ { key: 'details', label: t('translations.details'), icon: FileEdit }, @@ -222,12 +240,13 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, type="text" value={editTitle} onChange={e => setEditTitle(e.target.value)} - className="text-lg font-bold text-text-primary bg-transparent border-none outline-none focus:ring-0 w-full" + readOnly={isApproved} + className={`text-lg font-bold text-text-primary bg-transparent border-none outline-none focus:ring-0 w-full ${isApproved ? 'cursor-default' : ''}`} placeholder={t('translations.titlePlaceholder')} />
- + {translation.status?.replace('_', ' ')} @@ -244,7 +263,12 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} - footer={ + footer={isApproved ? ( +
+ + {t('translations.approvedReadOnly')} +
+ ) : (
- } + )} > {/* Details Tab */} {activeTab === 'details' && ( @@ -275,7 +299,8 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate, @@ -286,33 +311,71 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate,