All checks were successful
Deploy / deploy (push) Successful in 11s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
254 lines
11 KiB
JavaScript
254 lines
11 KiB
JavaScript
import { useState, useRef, useEffect } from 'react'
|
|
import { useLocation } from 'react-router-dom'
|
|
import { Bell, ChevronDown, LogOut, Shield, Lock, AlertCircle, CheckCircle } from 'lucide-react'
|
|
import { useAuth } from '../contexts/AuthContext'
|
|
import { getInitials, api } from '../utils/api'
|
|
import Modal from './Modal'
|
|
|
|
const pageTitles = {
|
|
'/': 'Dashboard',
|
|
'/posts': 'Post Production',
|
|
'/assets': 'Assets',
|
|
'/campaigns': 'Campaigns',
|
|
'/finance': 'Finance',
|
|
'/projects': 'Projects',
|
|
'/tasks': 'My Tasks',
|
|
'/team': 'Team',
|
|
'/users': 'User Management',
|
|
}
|
|
|
|
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: '✏️' },
|
|
}
|
|
|
|
export default function Header() {
|
|
const { user, logout } = useAuth()
|
|
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 (pageTitles[pathname]) return pageTitles[pathname]
|
|
if (pathname.startsWith('/projects/')) return 'Project Details'
|
|
if (pathname.startsWith('/campaigns/')) return 'Campaign Details'
|
|
return '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('New passwords do not match')
|
|
return
|
|
}
|
|
if (passwordForm.newPassword.length < 6) {
|
|
setPasswordError('New password must be at least 6 characters')
|
|
return
|
|
}
|
|
setPasswordSaving(true)
|
|
try {
|
|
await api.patch('/users/me/password', {
|
|
currentPassword: passwordForm.currentPassword,
|
|
newPassword: passwordForm.newPassword,
|
|
})
|
|
setPasswordSuccess('Password updated successfully')
|
|
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
|
setTimeout(() => setShowPasswordModal(false), 1500)
|
|
} catch (err) {
|
|
setPasswordError(err.message || 'Failed to change password')
|
|
} 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-4">
|
|
{/* Notifications */}
|
|
<button className="relative p-2 rounded-lg hover:bg-surface-tertiary text-text-secondary hover:text-text-primary transition-colors">
|
|
<Bell className="w-5 h-5" />
|
|
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full"></span>
|
|
</button>
|
|
|
|
{/* 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-left 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} {roleInfo.label}
|
|
</p>
|
|
</div>
|
|
<ChevronDown className={`w-4 h-4 text-text-tertiary transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
|
|
{showDropdown && (
|
|
<div className="absolute right-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>
|
|
{roleInfo.label}
|
|
</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-left"
|
|
>
|
|
<Shield className="w-4 h-4 text-text-tertiary" />
|
|
<span className="text-sm text-text-primary">User Management</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-left"
|
|
>
|
|
<Lock className="w-4 h-4 text-text-tertiary" />
|
|
<span className="text-sm text-text-primary">Change Password</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">Sign Out</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Change Password Modal */}
|
|
<Modal isOpen={showPasswordModal} onClose={() => setShowPasswordModal(false)} title="Change Password" size="md">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-primary mb-1">Current Password</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">New Password</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">Confirm New Password</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"
|
|
>
|
|
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'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
)
|
|
}
|