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:
fahed
2026-02-08 20:46:58 +03:00
commit 35d84b6bff
2240 changed files with 846749 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import { useState } from 'react'
import { Settings as SettingsIcon, Play, CheckCircle, Languages } from 'lucide-react'
import { api } from '../utils/api'
import { useLanguage } from '../i18n/LanguageContext'
export default function Settings() {
const { t, lang, setLang } = useLanguage()
const [restarting, setRestarting] = useState(false)
const [success, setSuccess] = useState(false)
const handleRestartTutorial = async () => {
setRestarting(true)
setSuccess(false)
try {
await api.patch('/users/me/tutorial', { completed: false })
setSuccess(true)
setTimeout(() => {
window.location.reload() // Reload to trigger tutorial
}, 1500)
} catch (err) {
console.error('Failed to restart tutorial:', err)
alert('Failed to restart tutorial')
} finally {
setRestarting(false)
}
}
return (
<div className="space-y-6 animate-fade-in max-w-3xl">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-text-primary flex items-center gap-3">
<SettingsIcon className="w-7 h-7 text-brand-primary" />
{t('settings.title')}
</h1>
<p className="text-sm text-text-tertiary mt-1">{t('settings.preferences')}</p>
</div>
{/* General Settings */}
<div className="bg-white rounded-xl border border-border overflow-hidden">
<div className="px-6 py-4 border-b border-border">
<h2 className="text-lg font-semibold text-text-primary">{t('settings.general')}</h2>
</div>
<div className="p-6 space-y-4">
{/* Language Selector */}
<div>
<label className="block text-sm font-medium text-text-primary mb-2 flex items-center gap-2">
<Languages className="w-4 h-4" />
{t('settings.language')}
</label>
<select
value={lang}
onChange={(e) => setLang(e.target.value)}
className="w-full max-w-xs px-4 py-2.5 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary bg-white"
>
<option value="en">{t('settings.english')}</option>
<option value="ar">{t('settings.arabic')}</option>
</select>
</div>
</div>
</div>
{/* Tutorial Section */}
<div className="bg-white rounded-xl border border-border overflow-hidden">
<div className="px-6 py-4 border-b border-border">
<h2 className="text-lg font-semibold text-text-primary">{t('settings.onboardingTutorial')}</h2>
</div>
<div className="p-6 space-y-4">
<p className="text-sm text-text-secondary">
{t('settings.tutorialDesc')}
</p>
<button
onClick={handleRestartTutorial}
disabled={restarting || success}
className="flex items-center gap-2 px-4 py-2.5 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 transition-colors"
>
{success ? (
<>
<CheckCircle className="w-4 h-4" />
{t('settings.tutorialRestarted')}
</>
) : (
<>
<Play className="w-4 h-4" />
{restarting ? t('settings.restarting') : t('settings.restartTutorial')}
</>
)}
</button>
{success && (
<p className="text-xs text-emerald-600 font-medium">
{t('settings.reloadingPage')}
</p>
)}
</div>
</div>
{/* More settings can go here in the future */}
<div className="bg-white rounded-xl border border-border overflow-hidden opacity-50">
<div className="px-6 py-4 border-b border-border">
<h2 className="text-lg font-semibold text-text-primary">{t('settings.moreComingSoon')}</h2>
</div>
<div className="p-6">
<p className="text-sm text-text-secondary">
{t('settings.additionalSettings')}
</p>
</div>
</div>
</div>
)
}