feat: post approval workflow, i18n completion, and multiple fixes
Deploy / deploy (push) Successful in 11s
Deploy / deploy (push) Successful in 11s
- Add approval process to posts (approver multi-select, rejected status column) - Reorganize PostDetailPanel into Content, Scheduling, Approval sections - Fix save button visibility: move to fixed footer via SlidePanel footer prop - Change date picker from datetime-local to date-only - Complete Arabic translations across all panels (Header, Issues, Artefacts) - Fix artefact versioning to start empty (copyFromPrevious defaults to false) - Separate media uploads by type (image, audio, video) in PostDetailPanel - Fix team membership save when editing own profile as superadmin - Server: add approver_ids column to Posts, enrich GET/POST/PATCH responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,29 +3,31 @@ import { useLocation } from 'react-router-dom'
|
||||
import { ChevronDown, LogOut, Shield, Lock, AlertCircle, CheckCircle } from 'lucide-react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { getInitials, api } from '../utils/api'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
import Modal from './Modal'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
|
||||
const pageTitles = {
|
||||
'/': 'Dashboard',
|
||||
'/posts': 'Post Production',
|
||||
'/assets': 'Assets',
|
||||
'/campaigns': 'Campaigns',
|
||||
'/finance': 'Finance',
|
||||
'/projects': 'Projects',
|
||||
'/tasks': 'My Tasks',
|
||||
'/team': 'Team',
|
||||
'/users': 'User Management',
|
||||
const PAGE_TITLE_KEYS = {
|
||||
'/': 'header.dashboard',
|
||||
'/posts': 'header.posts',
|
||||
'/assets': 'header.assets',
|
||||
'/campaigns': 'header.campaigns',
|
||||
'/finance': 'header.finance',
|
||||
'/projects': 'header.projects',
|
||||
'/tasks': 'header.tasks',
|
||||
'/team': 'header.team',
|
||||
'/users': 'header.users',
|
||||
}
|
||||
|
||||
const ROLE_INFO = {
|
||||
superadmin: { label: 'Superadmin', color: 'bg-purple-100 text-purple-700', icon: '👑' },
|
||||
manager: { label: 'Manager', color: 'bg-blue-100 text-blue-700', icon: '📊' },
|
||||
contributor: { label: 'Contributor', color: 'bg-green-100 text-green-700', icon: '✏️' },
|
||||
superadmin: { labelKey: 'header.superadmin', color: 'bg-purple-100 text-purple-700', icon: '👑' },
|
||||
manager: { labelKey: 'header.manager', color: 'bg-blue-100 text-blue-700', icon: '📊' },
|
||||
contributor: { labelKey: 'header.contributor', color: 'bg-green-100 text-green-700', icon: '✏️' },
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const { user, logout } = useAuth()
|
||||
const { t } = useLanguage()
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false)
|
||||
const [passwordForm, setPasswordForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
||||
@@ -36,10 +38,10 @@ export default function Header() {
|
||||
const location = useLocation()
|
||||
|
||||
function getPageTitle(pathname) {
|
||||
if (pageTitles[pathname]) return pageTitles[pathname]
|
||||
if (pathname.startsWith('/projects/')) return 'Project Details'
|
||||
if (pathname.startsWith('/campaigns/')) return 'Campaign Details'
|
||||
return 'Page'
|
||||
if (PAGE_TITLE_KEYS[pathname]) return t(PAGE_TITLE_KEYS[pathname])
|
||||
if (pathname.startsWith('/projects/')) return t('header.projectDetails')
|
||||
if (pathname.startsWith('/campaigns/')) return t('header.campaignDetails')
|
||||
return t('header.page')
|
||||
}
|
||||
const pageTitle = getPageTitle(location.pathname)
|
||||
|
||||
@@ -57,11 +59,11 @@ export default function Header() {
|
||||
setPasswordError('')
|
||||
setPasswordSuccess('')
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
setPasswordError('New passwords do not match')
|
||||
setPasswordError(t('header.passwordMismatch'))
|
||||
return
|
||||
}
|
||||
if (passwordForm.newPassword.length < 6) {
|
||||
setPasswordError('New password must be at least 6 characters')
|
||||
setPasswordError(t('header.passwordMinLength'))
|
||||
return
|
||||
}
|
||||
setPasswordSaving(true)
|
||||
@@ -70,11 +72,11 @@ export default function Header() {
|
||||
currentPassword: passwordForm.currentPassword,
|
||||
newPassword: passwordForm.newPassword,
|
||||
})
|
||||
setPasswordSuccess('Password updated successfully')
|
||||
setPasswordSuccess(t('header.passwordUpdateSuccess'))
|
||||
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
||||
setTimeout(() => setShowPasswordModal(false), 1500)
|
||||
} catch (err) {
|
||||
setPasswordError(err.message || 'Failed to change password')
|
||||
setPasswordError(err.message || t('header.passwordUpdateFailed'))
|
||||
} finally {
|
||||
setPasswordSaving(false)
|
||||
}
|
||||
@@ -121,7 +123,7 @@ export default function Header() {
|
||||
{user?.name || 'User'}
|
||||
</p>
|
||||
<p className={`text-[10px] font-medium ${roleInfo.color.split(' ')[1]}`}>
|
||||
{roleInfo.icon} {roleInfo.label}
|
||||
{roleInfo.icon} {t(roleInfo.labelKey)}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 text-text-tertiary transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
||||
@@ -135,7 +137,7 @@ export default function Header() {
|
||||
<p className="text-xs text-text-tertiary">{user?.email}</p>
|
||||
<div className={`inline-flex items-center gap-1 text-[10px] font-medium px-2 py-0.5 rounded-full mt-2 ${roleInfo.color}`}>
|
||||
<span>{roleInfo.icon}</span>
|
||||
{roleInfo.label}
|
||||
{t(roleInfo.labelKey)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -150,7 +152,7 @@ export default function Header() {
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-secondary transition-colors text-start"
|
||||
>
|
||||
<Shield className="w-4 h-4 text-text-tertiary" />
|
||||
<span className="text-sm text-text-primary">User Management</span>
|
||||
<span className="text-sm text-text-primary">{t('header.userManagement')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -159,7 +161,7 @@ export default function Header() {
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-secondary transition-colors text-start"
|
||||
>
|
||||
<Lock className="w-4 h-4 text-text-tertiary" />
|
||||
<span className="text-sm text-text-primary">Change Password</span>
|
||||
<span className="text-sm text-text-primary">{t('header.changePassword')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -170,7 +172,7 @@ export default function Header() {
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-red-50 transition-colors text-left group"
|
||||
>
|
||||
<LogOut className="w-4 h-4 text-text-tertiary group-hover:text-red-500" />
|
||||
<span className="text-sm text-text-primary group-hover:text-red-500">Sign Out</span>
|
||||
<span className="text-sm text-text-primary group-hover:text-red-500">{t('header.signOut')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,10 +182,10 @@ export default function Header() {
|
||||
</header>
|
||||
|
||||
{/* Change Password Modal */}
|
||||
<Modal isOpen={showPasswordModal} onClose={() => setShowPasswordModal(false)} title="Change Password" size="md">
|
||||
<Modal isOpen={showPasswordModal} onClose={() => setShowPasswordModal(false)} title={t('header.changePassword')} size="md">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Current Password</label>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('header.currentPassword')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordForm.currentPassword}
|
||||
@@ -193,7 +195,7 @@ export default function Header() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">New Password</label>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('header.newPassword')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordForm.newPassword}
|
||||
@@ -204,7 +206,7 @@ export default function Header() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Confirm New Password</label>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('header.confirmNewPassword')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordForm.confirmPassword}
|
||||
@@ -234,14 +236,14 @@ export default function Header() {
|
||||
onClick={() => setShowPasswordModal(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePasswordChange}
|
||||
disabled={!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword || passwordSaving}
|
||||
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||||
>
|
||||
{passwordSaving ? 'Saving...' : 'Update Password'}
|
||||
{passwordSaving ? t('header.saving') : t('header.updatePassword')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user