update on timeline on portfolio view + some corrections

This commit is contained in:
fahed
2026-02-10 13:20:49 +03:00
parent d15e54044e
commit 334727b232
37 changed files with 5119 additions and 1440 deletions

View File

@@ -9,6 +9,9 @@ import PostCard from '../components/PostCard'
import StatusBadge from '../components/StatusBadge'
import Modal from '../components/Modal'
import CommentsSection from '../components/CommentsSection'
import { SkeletonKanbanBoard, SkeletonTable } from '../components/SkeletonLoader'
import EmptyState from '../components/EmptyState'
import { useToast } from '../components/ToastContainer'
const EMPTY_POST = {
title: '', description: '', brand_id: '', platforms: [],
@@ -20,8 +23,10 @@ export default function PostProduction() {
const { t } = useLanguage()
const { teamMembers, brands } = useContext(AppContext)
const { canEditResource, canDeleteResource } = useAuth()
const toast = useToast()
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [view, setView] = useState('kanban')
const [showModal, setShowModal] = useState(false)
const [editingPost, setEditingPost] = useState(null)
@@ -59,6 +64,7 @@ export default function PostProduction() {
const handleSave = async () => {
setPublishError('')
setSaving(true)
try {
const data = {
title: formData.title,
@@ -82,14 +88,17 @@ export default function PostProduction() {
if (missingPlatforms.length > 0) {
const names = missingPlatforms.map(p => PLATFORMS[p]?.label || p).join(', ')
setPublishError(`${t('posts.publishMissing')} ${names}`)
setSaving(false)
return
}
}
if (editingPost) {
await api.patch(`/posts/${editingPost._id}`, data)
toast.success(t('posts.updated'))
} else {
await api.post('/posts', data)
toast.success(t('posts.created'))
}
setShowModal(false)
setEditingPost(null)
@@ -100,19 +109,27 @@ export default function PostProduction() {
console.error('Save failed:', err)
if (err.message?.includes('Cannot publish')) {
setPublishError(err.message.replace(/.*: /, ''))
} else {
toast.error(t('common.saveFailed'))
}
} finally {
setSaving(false)
}
}
const handleMovePost = async (postId, newStatus) => {
try {
await api.patch(`/posts/${postId}`, { status: newStatus })
toast.success(t('posts.statusUpdated'))
loadPosts()
} catch (err) {
console.error('Move failed:', err)
if (err.message?.includes('Cannot publish')) {
setMoveError(t('posts.publishRequired'))
setTimeout(() => setMoveError(''), 5000)
toast.error(t('posts.publishRequired'))
} else {
toast.error(t('common.updateFailed'))
}
}
}
@@ -153,8 +170,10 @@ export default function PostProduction() {
try {
await api.delete(`/attachments/${attachmentId}`)
if (editingPost) loadAttachments(editingPost._id || editingPost.id)
toast.success(t('posts.attachmentDeleted'))
} catch (err) {
console.error('Delete attachment failed:', err)
toast.error(t('common.deleteFailed'))
}
}
@@ -244,14 +263,7 @@ export default function PostProduction() {
})
if (loading) {
return (
<div className="space-y-4 animate-pulse">
<div className="h-12 bg-surface-tertiary rounded-xl"></div>
<div className="flex gap-4">
{[...Array(5)].map((_, i) => <div key={i} className="w-72 h-96 bg-surface-tertiary rounded-xl"></div>)}
</div>
</div>
)
return view === 'kanban' ? <SkeletonKanbanBoard /> : <SkeletonTable rows={8} cols={6} />
}
return (
@@ -342,30 +354,38 @@ export default function PostProduction() {
<KanbanBoard posts={filteredPosts} onPostClick={openEdit} onMovePost={handleMovePost} />
) : (
<div className="bg-white rounded-xl border border-border overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-border bg-surface-secondary">
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.postTitle')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.brand')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.status')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.platforms')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.assignTo')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.scheduledDate')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border-light">
{filteredPosts.map(post => (
<PostCard key={post._id} post={post} onClick={() => openEdit(post)} />
))}
{filteredPosts.length === 0 && (
<tr>
<td colSpan={6} className="py-12 text-center text-sm text-text-tertiary">
{t('posts.noPostsFound')}
</td>
{filteredPosts.length === 0 ? (
<EmptyState
icon={FileText}
title={posts.length === 0 ? t('posts.noPosts') : t('posts.noPostsFound')}
description={posts.length === 0 ? t('posts.createFirstPost') : t('posts.tryDifferentFilter')}
actionLabel={posts.length === 0 ? t('posts.createPost') : null}
onAction={posts.length === 0 ? openNew : null}
secondaryActionLabel={posts.length > 0 ? t('common.clearFilters') : null}
onSecondaryAction={() => {
setFilters({ brand: '', platform: '', assignedTo: '', campaign: '' })
setSearchTerm('')
}}
/>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-border bg-surface-secondary">
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.postTitle')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.brand')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.status')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.platforms')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.assignTo')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.scheduledDate')}</th>
</tr>
)}
</tbody>
</table>
</thead>
<tbody className="divide-y divide-border-light">
{filteredPosts.map(post => (
<PostCard key={post._id} post={post} onClick={() => openEdit(post)} />
))}
</tbody>
</table>
)}
</div>
)}
@@ -738,8 +758,8 @@ export default function PostProduction() {
</button>
<button
onClick={handleSave}
disabled={!formData.title}
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"
disabled={!formData.title || saving}
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 ${saving ? 'btn-loading' : ''}`}
>
{editingPost ? t('posts.saveChanges') : t('posts.createPost')}
</button>
@@ -759,11 +779,13 @@ export default function PostProduction() {
if (editingPost) {
try {
await api.delete(`/posts/${editingPost._id || editingPost.id}`)
toast.success(t('posts.deleted'))
setShowModal(false)
setEditingPost(null)
loadPosts()
} catch (err) {
console.error('Delete failed:', err)
toast.error(t('common.deleteFailed'))
}
}
}}