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:
110
client/src/pages/Settings.jsx
Normal file
110
client/src/pages/Settings.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user