Marketing Hub: RBAC, i18n (AR/EN), tasks overhaul, team/user merge, tutorial
Features: - Full RBAC with 3 roles (superadmin/manager/contributor) - Ownership tracking on posts, tasks, campaigns, projects - Task system: assign to anyone, filter combobox, visibility scoping - Team members merged into users table (single source of truth) - Post thumbnails on kanban cards from attachments - Publication link validation before publishing - Interactive onboarding tutorial with Settings restart - Full Arabic/English i18n with RTL layout support - Language toggle in sidebar, IBM Plex Sans Arabic font - Brand-based visibility filtering for non-superadmins - Manager can only create contributors - Profile completion flow for new users - Cookie-based sessions (express-session + SQLite)
This commit is contained in:
175
client/src/App.jsx
Normal file
175
client/src/App.jsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { useState, useEffect, createContext } from 'react'
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||
import { LanguageProvider } from './i18n/LanguageContext'
|
||||
import Layout from './components/Layout'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import PostProduction from './pages/PostProduction'
|
||||
import Assets from './pages/Assets'
|
||||
import Campaigns from './pages/Campaigns'
|
||||
import CampaignDetail from './pages/CampaignDetail'
|
||||
import Finance from './pages/Finance'
|
||||
import Projects from './pages/Projects'
|
||||
import ProjectDetail from './pages/ProjectDetail'
|
||||
import Tasks from './pages/Tasks'
|
||||
import Team from './pages/Team'
|
||||
import Users from './pages/Users'
|
||||
import Settings from './pages/Settings'
|
||||
import Login from './pages/Login'
|
||||
import Tutorial from './components/Tutorial'
|
||||
import { api } from './utils/api'
|
||||
import { useLanguage } from './i18n/LanguageContext'
|
||||
|
||||
export const AppContext = createContext()
|
||||
|
||||
function AppContent() {
|
||||
const { user, loading: authLoading } = useAuth()
|
||||
const { t } = useLanguage()
|
||||
const [teamMembers, setTeamMembers] = useState([])
|
||||
const [brands, setBrands] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showTutorial, setShowTutorial] = useState(false)
|
||||
const [showProfilePrompt, setShowProfilePrompt] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (user && !authLoading) {
|
||||
loadInitialData()
|
||||
// Check if tutorial should be shown
|
||||
if (user.tutorial_completed === 0) {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
// Check if profile is incomplete
|
||||
if (!user.profileComplete && user.role !== 'superadmin') {
|
||||
setShowProfilePrompt(true)
|
||||
}
|
||||
} else if (!authLoading) {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [user, authLoading])
|
||||
|
||||
const loadTeam = async () => {
|
||||
try {
|
||||
const data = await api.get('/users/team')
|
||||
const members = Array.isArray(data) ? data : (data.data || [])
|
||||
setTeamMembers(members)
|
||||
return members
|
||||
} catch (err) {
|
||||
console.error('Failed to load team:', err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
const [members, brandsData] = await Promise.all([
|
||||
loadTeam(),
|
||||
api.get('/brands').then(d => Array.isArray(d) ? d : (d.data || [])).catch(() => []),
|
||||
])
|
||||
setTeamMembers(members)
|
||||
setBrands(brandsData)
|
||||
} catch (err) {
|
||||
console.error('Failed to load initial data:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTutorialComplete = async () => {
|
||||
try {
|
||||
await api.patch('/users/me/tutorial', { completed: true })
|
||||
setShowTutorial(false)
|
||||
} catch (err) {
|
||||
console.error('Failed to complete tutorial:', err)
|
||||
}
|
||||
}
|
||||
|
||||
if (authLoading || loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-surface-secondary">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-brand-primary border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-text-secondary font-medium">{t('dashboard.loadingHub')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ currentUser: user, teamMembers, brands, loadTeam }}>
|
||||
{/* Profile completion prompt */}
|
||||
{showProfilePrompt && (
|
||||
<div className="fixed top-4 right-4 z-50 bg-amber-50 border-2 border-amber-400 rounded-xl shadow-lg p-4 max-w-md animate-fade-in">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-amber-400 flex items-center justify-center text-white shrink-0">
|
||||
⚠️
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-amber-900 mb-1">{t('profile.completeYourProfile')}</h3>
|
||||
<p className="text-sm text-amber-800 mb-3">
|
||||
{t('profile.completeDesc')}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href="/team"
|
||||
className="px-3 py-1.5 bg-amber-400 text-white text-sm font-medium rounded-lg hover:bg-amber-500 transition-colors"
|
||||
>
|
||||
{t('profile.completeProfileBtn')}
|
||||
</a>
|
||||
<button
|
||||
onClick={() => setShowProfilePrompt(false)}
|
||||
className="px-3 py-1.5 text-sm font-medium text-amber-800 hover:bg-amber-100 rounded-lg transition-colors"
|
||||
>
|
||||
{t('profile.later')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowProfilePrompt(false)}
|
||||
className="text-amber-600 hover:text-amber-800 transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tutorial overlay */}
|
||||
{showTutorial && <Tutorial onComplete={handleTutorialComplete} />}
|
||||
|
||||
<Routes>
|
||||
<Route path="/login" element={user ? <Navigate to="/" replace /> : <Login />} />
|
||||
<Route path="/" element={user ? <Layout /> : <Navigate to="/login" replace />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="posts" element={<PostProduction />} />
|
||||
<Route path="assets" element={<Assets />} />
|
||||
<Route path="campaigns" element={<Campaigns />} />
|
||||
<Route path="campaigns/:id" element={<CampaignDetail />} />
|
||||
{(user?.role === 'superadmin' || user?.role === 'manager') && (
|
||||
<Route path="finance" element={<Finance />} />
|
||||
)}
|
||||
<Route path="projects" element={<Projects />} />
|
||||
<Route path="projects/:id" element={<ProjectDetail />} />
|
||||
<Route path="tasks" element={<Tasks />} />
|
||||
<Route path="team" element={<Team />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
{user?.role === 'superadmin' && (
|
||||
<Route path="users" element={<Users />} />
|
||||
)}
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<LanguageProvider>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</LanguageProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
Reference in New Issue
Block a user