Files
marketing-app/client/src/components/Header.jsx
T
fahed 96fb838388
Deploy / deploy (push) Successful in 12s
fix: consistent page titles for all routes in header
Added missing title keys for calendar, artefacts, brands, budgets,
issues, and settings. No more fallback "Page" text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:46:57 +03:00

259 lines
11 KiB
React

import { useState, useRef, useEffect } from 'react'
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 PAGE_TITLE_KEYS = {
'/': 'header.dashboard',
'/posts': 'header.posts',
'/calendar': 'header.calendar',
'/assets': 'header.assets',
'/artefacts': 'header.artefacts',
'/campaigns': 'header.campaigns',
'/brands': 'header.brands',
'/finance': 'header.finance',
'/budgets': 'header.budgets',
'/projects': 'header.projects',
'/tasks': 'header.tasks',
'/issues': 'header.issues',
'/team': 'header.team',
'/settings': 'header.settings',
}
const ROLE_INFO = {
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: '' })
const [passwordError, setPasswordError] = useState('')
const [passwordSuccess, setPasswordSuccess] = useState('')
const [passwordSaving, setPasswordSaving] = useState(false)
const dropdownRef = useRef(null)
const location = useLocation()
function getPageTitle(pathname) {
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)
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setShowDropdown(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const handlePasswordChange = async () => {
setPasswordError('')
setPasswordSuccess('')
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setPasswordError(t('header.passwordMismatch'))
return
}
if (passwordForm.newPassword.length < 6) {
setPasswordError(t('header.passwordMinLength'))
return
}
setPasswordSaving(true)
try {
await api.patch('/users/me/password', {
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword,
})
setPasswordSuccess(t('header.passwordUpdateSuccess'))
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' })
setTimeout(() => setShowPasswordModal(false), 1500)
} catch (err) {
setPasswordError(err.message || t('header.passwordUpdateFailed'))
} finally {
setPasswordSaving(false)
}
}
const openPasswordModal = () => {
setShowDropdown(false)
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' })
setPasswordError('')
setPasswordSuccess('')
setShowPasswordModal(true)
}
const roleInfo = ROLE_INFO[user?.role] || ROLE_INFO.contributor
return (
<>
<header className="h-16 bg-white border-b border-border flex items-center justify-between px-6 shrink-0 sticky top-0 z-20">
{/* Page title */}
<div>
<h2 className="text-xl font-bold text-text-primary">{pageTitle}</h2>
</div>
{/* Right side */}
<div className="flex items-center gap-2">
{/* Theme toggle */}
<ThemeToggle />
{/* User menu */}
<div ref={dropdownRef} className="relative">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-surface-tertiary transition-colors"
>
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-semibold ${
user?.role === 'superadmin'
? 'bg-gradient-to-br from-purple-500 to-pink-500'
: 'bg-gradient-to-br from-blue-500 to-indigo-500'
}`}>
{getInitials(user?.name)}
</div>
<div className="text-start hidden sm:block">
<p className="text-sm font-medium text-text-primary">
{user?.name || 'User'}
</p>
<p className={`text-[10px] font-medium ${roleInfo.color.split(' ')[1]}`}>
{roleInfo.icon} {t(roleInfo.labelKey)}
</p>
</div>
<ChevronDown className={`w-4 h-4 text-text-tertiary transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
</button>
{showDropdown && (
<div className="absolute end-0 top-full mt-2 w-64 bg-white rounded-xl shadow-lg border border-border overflow-hidden animate-scale-in">
{/* User info */}
<div className="px-4 py-3 border-b border-border-light bg-surface-secondary">
<p className="text-sm font-semibold text-text-primary">{user?.name}</p>
<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>
{t(roleInfo.labelKey)}
</div>
</div>
{/* Menu items */}
<div className="py-2">
{user?.role === 'superadmin' && (
<button
onClick={() => {
setShowDropdown(false)
window.location.href = '/users'
}}
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">{t('header.userManagement')}</span>
</button>
)}
<button
onClick={openPasswordModal}
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">{t('header.changePassword')}</span>
</button>
<button
onClick={() => {
setShowDropdown(false)
logout()
}}
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">{t('header.signOut')}</span>
</button>
</div>
</div>
)}
</div>
</div>
</header>
{/* Change Password Modal */}
<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">{t('header.currentPassword')}</label>
<input
type="password"
value={passwordForm.currentPassword}
onChange={e => { setPasswordForm(f => ({ ...f, currentPassword: e.target.value })); setPasswordError('') }}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="••••••••"
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('header.newPassword')}</label>
<input
type="password"
value={passwordForm.newPassword}
onChange={e => { setPasswordForm(f => ({ ...f, newPassword: e.target.value })); setPasswordError('') }}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="••••••••"
minLength={6}
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('header.confirmNewPassword')}</label>
<input
type="password"
value={passwordForm.confirmPassword}
onChange={e => { setPasswordForm(f => ({ ...f, confirmPassword: e.target.value })); setPasswordError('') }}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="••••••••"
minLength={6}
/>
</div>
{passwordError && (
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<AlertCircle className="w-4 h-4 text-red-500 shrink-0" />
<p className="text-sm text-red-500">{passwordError}</p>
</div>
)}
{passwordSuccess && (
<div className="flex items-center gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
<CheckCircle className="w-4 h-4 text-green-500 shrink-0" />
<p className="text-sm text-green-500">{passwordSuccess}</p>
</div>
)}
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button
onClick={() => setShowPasswordModal(false)}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
>
{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 ? t('header.saving') : t('header.updatePassword')}
</button>
</div>
</div>
</Modal>
</>
)
}