Dashboard fix, expense system, currency settings, visual upgrade
- Fix Dashboard stat card: show "Budget Remaining" instead of "Budget Spent" with correct remaining value accounting for campaign allocations - Add expense system: budget entries now have income/expense type with server-side split, per-campaign and per-project expense tracking, colored amounts, type filters, and summary bar in Budgets page - Add configurable currency in Settings (SAR default, supports 10 currencies) replacing all hardcoded SAR references across the app - Replace PiggyBank icon with Landmark (culturally appropriate for KSA) - Visual upgrade: mesh background, gradient text, premium stat cards with accent bars, section-card containers, sidebar active glow - UX polish: consistent text-2xl headers, skeleton loaders for Finance and Budgets pages - Finance page: expenses column in campaign/project breakdown tables, ROI accounts for expenses, expense stat card Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+233
-56
@@ -1,13 +1,14 @@
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useContext, useEffect, useState, useMemo } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { format, isAfter, isBefore, addDays } from 'date-fns'
|
||||
import { FileText, Megaphone, AlertTriangle, ArrowRight, Clock, Wallet, TrendingUp, DollarSign, PiggyBank } from 'lucide-react'
|
||||
import { FileText, Megaphone, AlertTriangle, ArrowRight, Clock, Wallet, TrendingUp, TrendingDown, DollarSign, Landmark, CheckSquare, FolderKanban } from 'lucide-react'
|
||||
import { AppContext } from '../App'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
import { api, PRIORITY_CONFIG } from '../utils/api'
|
||||
import StatCard from '../components/StatCard'
|
||||
import StatusBadge from '../components/StatusBadge'
|
||||
import BrandBadge from '../components/BrandBadge'
|
||||
import DatePresetPicker from '../components/DatePresetPicker'
|
||||
import { SkeletonDashboard } from '../components/SkeletonLoader'
|
||||
|
||||
function getBudgetBarColor(percentage) {
|
||||
@@ -17,14 +18,20 @@ function getBudgetBarColor(percentage) {
|
||||
}
|
||||
|
||||
function FinanceMini({ finance }) {
|
||||
const { t } = useLanguage()
|
||||
const { t, currencySymbol } = useLanguage()
|
||||
if (!finance) return null
|
||||
const totalReceived = finance.totalReceived || 0
|
||||
const spent = finance.spent || 0
|
||||
const remaining = finance.remaining || 0
|
||||
const roi = finance.roi || 0
|
||||
const totalExpenses = finance.totalExpenses || 0
|
||||
const campaignBudget = finance.totalCampaignBudget || 0
|
||||
const projectBudget = finance.totalProjectBudget || 0
|
||||
const unallocated = finance.unallocated ?? (totalReceived - campaignBudget - projectBudget)
|
||||
const pct = totalReceived > 0 ? (spent / totalReceived) * 100 : 0
|
||||
const barColor = getBudgetBarColor(pct)
|
||||
const campPct = totalReceived > 0 ? (campaignBudget / totalReceived) * 100 : 0
|
||||
const projPct = totalReceived > 0 ? (projectBudget / totalReceived) * 100 : 0
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-border p-5">
|
||||
@@ -37,35 +44,55 @@ function FinanceMini({ finance }) {
|
||||
|
||||
{totalReceived === 0 ? (
|
||||
<div className="text-center py-6 text-sm text-text-tertiary">
|
||||
{t('dashboard.noBudgetRecorded')}. <Link to="/finance" className="text-brand-primary underline">{t('dashboard.addBudget')}</Link>
|
||||
{t('dashboard.noBudgetRecorded')}. <Link to="/budgets" className="text-brand-primary underline">{t('dashboard.addBudget')}</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Budget bar */}
|
||||
<div className="mb-4">
|
||||
{/* Spending bar */}
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between text-xs text-text-tertiary mb-1">
|
||||
<span>{spent.toLocaleString()} {t('dashboard.sar')} {t('dashboard.spent')}</span>
|
||||
<span>{totalReceived.toLocaleString()} {t('dashboard.sar')} {t('dashboard.received')}</span>
|
||||
<span>{spent.toLocaleString()} {currencySymbol} {t('dashboard.spent')}</span>
|
||||
<span>{totalReceived.toLocaleString()} {currencySymbol} {t('dashboard.received')}</span>
|
||||
</div>
|
||||
<div className="h-3 bg-surface-tertiary rounded-full overflow-hidden">
|
||||
<div className="h-2.5 bg-surface-tertiary rounded-full overflow-hidden">
|
||||
<div className={`h-full ${barColor} rounded-full transition-all`} style={{ width: `${Math.min(pct, 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Allocation bar */}
|
||||
{(campaignBudget > 0 || projectBudget > 0) && (
|
||||
<div className="mb-3">
|
||||
<div className="text-[10px] text-text-tertiary mb-1 font-medium uppercase tracking-wide">Allocation</div>
|
||||
<div className="h-2 bg-surface-tertiary rounded-full overflow-hidden flex">
|
||||
{campPct > 0 && <div className="h-full bg-blue-500" style={{ width: `${campPct}%` }} />}
|
||||
{projPct > 0 && <div className="h-full bg-purple-500" style={{ width: `${projPct}%` }} />}
|
||||
</div>
|
||||
<div className="flex gap-3 mt-1 text-[10px] text-text-tertiary">
|
||||
{campaignBudget > 0 && <span><span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-500 mr-1" />{campaignBudget.toLocaleString()}</span>}
|
||||
{projectBudget > 0 && <span><span className="inline-block w-1.5 h-1.5 rounded-full bg-purple-500 mr-1" />{projectBudget.toLocaleString()}</span>}
|
||||
{unallocated > 0 && <span><span className="inline-block w-1.5 h-1.5 rounded-full bg-gray-300 mr-1" />{unallocated.toLocaleString()} free</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Key numbers */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className={`grid ${totalExpenses > 0 ? 'grid-cols-3' : 'grid-cols-2'} gap-3`}>
|
||||
<div className="text-center p-2 bg-surface-secondary rounded-lg">
|
||||
<PiggyBank className="w-4 h-4 mx-auto mb-1 text-emerald-500" />
|
||||
<Landmark className="w-4 h-4 mx-auto mb-1 text-emerald-500" />
|
||||
<div className={`text-sm font-bold ${remaining >= 0 ? 'text-emerald-600' : 'text-red-600'}`}>
|
||||
{remaining.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-[10px] text-text-tertiary">{t('dashboard.remaining')}</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-surface-secondary rounded-lg">
|
||||
<DollarSign className="w-4 h-4 mx-auto mb-1 text-purple-500" />
|
||||
<div className="text-sm font-bold text-purple-600">{(finance.revenue || 0).toLocaleString()}</div>
|
||||
<div className="text-[10px] text-text-tertiary">{t('dashboard.revenue')}</div>
|
||||
</div>
|
||||
{totalExpenses > 0 && (
|
||||
<div className="text-center p-2 bg-surface-secondary rounded-lg">
|
||||
<TrendingDown className="w-4 h-4 mx-auto mb-1 text-red-500" />
|
||||
<div className="text-sm font-bold text-red-600">
|
||||
{totalExpenses.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-[10px] text-text-tertiary">{t('dashboard.expenses')}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center p-2 bg-surface-secondary rounded-lg">
|
||||
<TrendingUp className="w-4 h-4 mx-auto mb-1 text-blue-500" />
|
||||
<div className={`text-sm font-bold ${roi >= 0 ? 'text-emerald-600' : 'text-red-600'}`}>
|
||||
@@ -81,22 +108,22 @@ function FinanceMini({ finance }) {
|
||||
}
|
||||
|
||||
function ActiveCampaignsList({ campaigns, finance }) {
|
||||
const { t } = useLanguage()
|
||||
const { t, currencySymbol } = useLanguage()
|
||||
const active = campaigns.filter(c => c.status === 'active')
|
||||
const campaignData = (finance?.campaigns || []).filter(c => c.status === 'active')
|
||||
|
||||
if (active.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-border">
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<div className="section-card">
|
||||
<div className="section-card-header flex items-center justify-between">
|
||||
<h3 className="font-semibold text-text-primary">{t('dashboard.activeCampaigns')}</h3>
|
||||
<Link to="/campaigns" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
||||
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-border-light">
|
||||
{active.map(c => {
|
||||
{active.slice(0, 5).map(c => {
|
||||
const cd = campaignData.find(d => d.id === (c._id || c.id)) || {}
|
||||
const spent = cd.tracks_spent || 0
|
||||
const allocated = cd.tracks_allocated || 0
|
||||
@@ -110,7 +137,7 @@ function ActiveCampaignsList({ campaigns, finance }) {
|
||||
<div className="mt-1.5 w-32">
|
||||
<div className="flex justify-between text-[9px] text-text-tertiary mb-0.5">
|
||||
<span>{spent.toLocaleString()}</span>
|
||||
<span>{allocated.toLocaleString()} SAR</span>
|
||||
<span>{allocated.toLocaleString()} {currencySymbol}</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-surface-tertiary rounded-full overflow-hidden">
|
||||
<div className={`h-full ${barColor} rounded-full`} style={{ width: `${Math.min(pct, 100)}%` }} />
|
||||
@@ -121,7 +148,7 @@ function ActiveCampaignsList({ campaigns, finance }) {
|
||||
<div className="text-right shrink-0">
|
||||
{cd.tracks_impressions > 0 && (
|
||||
<div className="text-[10px] text-text-tertiary">
|
||||
👁 {cd.tracks_impressions.toLocaleString()} · 🖱 {cd.tracks_clicks.toLocaleString()}
|
||||
{cd.tracks_impressions.toLocaleString()} imp. / {cd.tracks_clicks.toLocaleString()} clicks
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -133,31 +160,140 @@ function ActiveCampaignsList({ campaigns, finance }) {
|
||||
)
|
||||
}
|
||||
|
||||
function MyTasksList({ tasks, currentUserId, navigate, t }) {
|
||||
const myTasks = tasks
|
||||
.filter(task => {
|
||||
const assignedId = task.assigned_to_id || task.assignedTo
|
||||
return assignedId === currentUserId && task.status !== 'done'
|
||||
})
|
||||
.slice(0, 5)
|
||||
|
||||
return (
|
||||
<div className="section-card">
|
||||
<div className="section-card-header flex items-center justify-between">
|
||||
<h3 className="font-semibold text-text-primary flex items-center gap-2">
|
||||
<CheckSquare className="w-4 h-4 text-brand-primary" />
|
||||
{t('dashboard.myTasks')}
|
||||
</h3>
|
||||
<Link to="/tasks" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
||||
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-border-light">
|
||||
{myTasks.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-text-tertiary">
|
||||
{t('dashboard.allOnTrack')}
|
||||
</div>
|
||||
) : (
|
||||
myTasks.map(task => (
|
||||
<div
|
||||
key={task._id || task.id}
|
||||
onClick={() => navigate('/tasks')}
|
||||
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||
>
|
||||
<div className={`w-2 h-2 rounded-full shrink-0 ${(PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium).color}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.title}</p>
|
||||
<StatusBadge status={task.status} size="xs" />
|
||||
</div>
|
||||
{task.dueDate && (
|
||||
<div className="flex items-center gap-1 text-xs text-text-tertiary shrink-0">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{format(new Date(task.dueDate), 'MMM d')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectProgress({ projects, tasks, t }) {
|
||||
if (!projects || projects.length === 0) return null
|
||||
|
||||
const activeProjects = projects
|
||||
.filter(p => p.status === 'active' || p.status === 'in_progress')
|
||||
.slice(0, 5)
|
||||
|
||||
if (activeProjects.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="section-card">
|
||||
<div className="section-card-header flex items-center justify-between">
|
||||
<h3 className="font-semibold text-text-primary flex items-center gap-2">
|
||||
<FolderKanban className="w-4 h-4 text-purple-500" />
|
||||
{t('dashboard.projectProgress')}
|
||||
</h3>
|
||||
<Link to="/projects" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
||||
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-border-light">
|
||||
{activeProjects.map(project => {
|
||||
const projectId = project._id || project.id
|
||||
const projectTasks = tasks.filter(t => (t.project_id || t.projectId) === projectId)
|
||||
const doneTasks = projectTasks.filter(t => t.status === 'done').length
|
||||
const totalTasks = projectTasks.length
|
||||
const pct = totalTasks > 0 ? (doneTasks / totalTasks) * 100 : 0
|
||||
|
||||
return (
|
||||
<Link key={projectId} to={`/projects/${projectId}`} className="flex items-center gap-4 px-5 py-3 hover:bg-surface-secondary transition-colors">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{project.name}</p>
|
||||
<div className="flex items-center gap-2 mt-1.5">
|
||||
<div className="flex-1 h-1.5 bg-surface-tertiary rounded-full overflow-hidden max-w-[120px]">
|
||||
<div className="h-full bg-purple-500 rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-text-tertiary shrink-0">
|
||||
{doneTasks}/{totalTasks} {t('tasks.tasks')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={project.status} size="xs" />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useLanguage()
|
||||
const { t, currencySymbol } = useLanguage()
|
||||
const navigate = useNavigate()
|
||||
const { currentUser, teamMembers } = useContext(AppContext)
|
||||
const [posts, setPosts] = useState([])
|
||||
const [campaigns, setCampaigns] = useState([])
|
||||
const [tasks, setTasks] = useState([])
|
||||
const [projects, setProjects] = useState([])
|
||||
const [finance, setFinance] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Date filtering
|
||||
const [dateFrom, setDateFrom] = useState('')
|
||||
const [dateTo, setDateTo] = useState('')
|
||||
const [activePreset, setActivePreset] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [postsRes, campaignsRes, tasksRes, financeRes] = await Promise.allSettled([
|
||||
api.get('/posts?limit=10&sort=-createdAt'),
|
||||
const [postsRes, campaignsRes, tasksRes, financeRes, projectsRes] = await Promise.allSettled([
|
||||
api.get('/posts?limit=50&sort=-createdAt'),
|
||||
api.get('/campaigns'),
|
||||
api.get('/tasks'),
|
||||
api.get('/finance/summary'),
|
||||
api.get('/projects'),
|
||||
])
|
||||
setPosts(postsRes.status === 'fulfilled' ? (postsRes.value.data || postsRes.value || []) : [])
|
||||
setCampaigns(campaignsRes.status === 'fulfilled' ? (campaignsRes.value.data || campaignsRes.value || []) : [])
|
||||
setTasks(tasksRes.status === 'fulfilled' ? (tasksRes.value.data || tasksRes.value || []) : [])
|
||||
setFinance(financeRes.status === 'fulfilled' ? (financeRes.value.data || financeRes.value || null) : null)
|
||||
setProjects(projectsRes.status === 'fulfilled' ? (projectsRes.value.data || projectsRes.value || []) : [])
|
||||
} catch (err) {
|
||||
console.error('Dashboard load error:', err)
|
||||
} finally {
|
||||
@@ -165,12 +301,35 @@ export default function Dashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
// Filtered data based on date range
|
||||
const filteredPosts = useMemo(() => {
|
||||
if (!dateFrom && !dateTo) return posts
|
||||
return posts.filter(p => {
|
||||
const d = p.scheduled_date || p.scheduledDate
|
||||
if (!d) return true
|
||||
if (dateFrom && d < dateFrom) return false
|
||||
if (dateTo && d > dateTo) return false
|
||||
return true
|
||||
})
|
||||
}, [posts, dateFrom, dateTo])
|
||||
|
||||
const filteredTasks = useMemo(() => {
|
||||
if (!dateFrom && !dateTo) return tasks
|
||||
return tasks.filter(t => {
|
||||
const d = t.due_date || t.dueDate
|
||||
if (!d) return true
|
||||
if (dateFrom && d < dateFrom) return false
|
||||
if (dateTo && d > dateTo) return false
|
||||
return true
|
||||
})
|
||||
}, [tasks, dateFrom, dateTo])
|
||||
|
||||
const activeCampaigns = campaigns.filter(c => c.status === 'active').length
|
||||
const overdueTasks = tasks.filter(t =>
|
||||
const overdueTasks = filteredTasks.filter(t =>
|
||||
t.dueDate && new Date(t.dueDate) < new Date() && t.status !== 'done'
|
||||
).length
|
||||
|
||||
const upcomingDeadlines = tasks
|
||||
const upcomingDeadlines = filteredTasks
|
||||
.filter(t => {
|
||||
if (!t.dueDate || t.status === 'done') return false
|
||||
const due = new Date(t.dueDate)
|
||||
@@ -186,14 +345,21 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* Welcome */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-primary">
|
||||
{t('dashboard.welcomeBack')}, {currentUser?.name || 'there'}
|
||||
</h1>
|
||||
<p className="text-text-secondary mt-1">
|
||||
{t('dashboard.happeningToday')}
|
||||
</p>
|
||||
{/* Welcome + Date presets */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gradient">
|
||||
{t('dashboard.welcomeBack')}, {currentUser?.name || 'there'}
|
||||
</h1>
|
||||
<p className="text-text-secondary mt-1">
|
||||
{t('dashboard.happeningToday')}
|
||||
</p>
|
||||
</div>
|
||||
<DatePresetPicker
|
||||
activePreset={activePreset}
|
||||
onSelect={(from, to, key) => { setDateFrom(from); setDateTo(to); setActivePreset(key) }}
|
||||
onClear={() => { setDateFrom(''); setDateTo(''); setActivePreset('') }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
@@ -201,8 +367,8 @@ export default function Dashboard() {
|
||||
<StatCard
|
||||
icon={FileText}
|
||||
label={t('dashboard.totalPosts')}
|
||||
value={posts.length || 0}
|
||||
subtitle={`${posts.filter(p => p.status === 'published').length} ${t('dashboard.published')}`}
|
||||
value={filteredPosts.length || 0}
|
||||
subtitle={`${filteredPosts.filter(p => p.status === 'published').length} ${t('dashboard.published')}`}
|
||||
color="brand-primary"
|
||||
/>
|
||||
<StatCard
|
||||
@@ -213,10 +379,10 @@ export default function Dashboard() {
|
||||
color="brand-secondary"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Wallet}
|
||||
label={t('dashboard.budgetSpent')}
|
||||
value={`${(finance?.spent || 0).toLocaleString()}`}
|
||||
subtitle={finance?.totalReceived ? `${t('dashboard.of')} ${finance.totalReceived.toLocaleString()} ${t('dashboard.sar')}` : t('dashboard.noBudget')}
|
||||
icon={Landmark}
|
||||
label={t('dashboard.budgetRemaining')}
|
||||
value={`${(finance?.remaining ?? 0).toLocaleString()}`}
|
||||
subtitle={finance?.totalReceived ? `${(finance.spent || 0).toLocaleString()} ${t('dashboard.spent')} ${t('dashboard.of')} ${finance.totalReceived.toLocaleString()} ${currencySymbol}` : t('dashboard.noBudget')}
|
||||
color="brand-tertiary"
|
||||
/>
|
||||
<StatCard
|
||||
@@ -228,35 +394,42 @@ export default function Dashboard() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Three columns on large, stack on small */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Budget Overview */}
|
||||
<FinanceMini finance={finance} />
|
||||
{/* My Tasks + Project Progress */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<MyTasksList tasks={filteredTasks} currentUserId={currentUser?.id || currentUser?._id} navigate={navigate} t={t} />
|
||||
<ProjectProgress projects={projects} tasks={tasks} t={t} />
|
||||
</div>
|
||||
|
||||
{/* Active Campaigns with budget bars */}
|
||||
{/* Budget + Active Campaigns */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<FinanceMini finance={finance} />
|
||||
<div className="lg:col-span-2">
|
||||
<ActiveCampaignsList campaigns={campaigns} finance={finance} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two columns */}
|
||||
{/* Recent Posts + Upcoming Deadlines */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Recent Posts */}
|
||||
<div className="bg-white rounded-xl border border-border">
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<div className="section-card">
|
||||
<div className="section-card-header flex items-center justify-between">
|
||||
<h3 className="font-semibold text-text-primary">{t('dashboard.recentPosts')}</h3>
|
||||
<Link to="/posts" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
||||
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-border-light">
|
||||
{posts.length === 0 ? (
|
||||
{filteredPosts.length === 0 ? (
|
||||
<div className="py-12 text-center text-sm text-text-tertiary">
|
||||
{t('dashboard.noPostsYet')}
|
||||
</div>
|
||||
) : (
|
||||
posts.slice(0, 8).map((post) => (
|
||||
<div key={post._id} className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors">
|
||||
filteredPosts.slice(0, 8).map((post) => (
|
||||
<div
|
||||
key={post._id}
|
||||
onClick={() => navigate('/posts')}
|
||||
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{post.title}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
@@ -271,8 +444,8 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Upcoming Deadlines */}
|
||||
<div className="bg-white rounded-xl border border-border">
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<div className="section-card">
|
||||
<div className="section-card-header flex items-center justify-between">
|
||||
<h3 className="font-semibold text-text-primary">{t('dashboard.upcomingDeadlines')}</h3>
|
||||
<Link to="/tasks" className="text-sm text-brand-primary hover:text-brand-primary-light font-medium flex items-center gap-1">
|
||||
{t('dashboard.viewAll')} <ArrowRight className="w-3.5 h-3.5" />
|
||||
@@ -285,7 +458,11 @@ export default function Dashboard() {
|
||||
</div>
|
||||
) : (
|
||||
upcomingDeadlines.map((task) => (
|
||||
<div key={task._id} className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors">
|
||||
<div
|
||||
key={task._id}
|
||||
onClick={() => navigate('/tasks')}
|
||||
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary transition-colors cursor-pointer"
|
||||
>
|
||||
<div className={`w-2 h-2 rounded-full ${(PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium).color}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.title}</p>
|
||||
|
||||
Reference in New Issue
Block a user