@actuate-media/cms-admin 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/lib/page-editor-service-api.test.d.ts +2 -0
- package/dist/__tests__/lib/page-editor-service-api.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-editor-service-api.test.js +155 -0
- package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -0
- package/dist/__tests__/lib/pages-service-fetch.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-service-fetch.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-service-fetch.test.js +125 -0
- package/dist/__tests__/lib/pages-service-fetch.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +75 -23
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- package/dist/__tests__/lib/posts-service.test.js +132 -142
- package/dist/__tests__/lib/posts-service.test.js.map +1 -1
- package/dist/__tests__/views/page-section-editor.test.d.ts +2 -0
- package/dist/__tests__/views/page-section-editor.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-section-editor.test.js +56 -0
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.d.ts +2 -0
- package/dist/__tests__/views/pages-list-view.test.d.ts.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.js +102 -0
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -0
- package/dist/__tests__/views/posts-list-view.render.test.d.ts +2 -0
- package/dist/__tests__/views/posts-list-view.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/posts-list-view.render.test.js +73 -0
- package/dist/__tests__/views/posts-list-view.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +1 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +55 -0
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/pages-service.d.ts +26 -0
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +65 -38
- package/dist/lib/pages-service.js.map +1 -1
- package/dist/lib/post-editor-service.d.ts +6 -4
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +32 -37
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/lib/posts-service.d.ts.map +1 -1
- package/dist/lib/posts-service.js +91 -160
- package/dist/lib/posts-service.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +71 -46
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Posts/NewPostPicker.d.ts +5 -4
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -1
- package/dist/views/Posts/NewPostPicker.js +5 -4
- package/dist/views/Posts/NewPostPicker.js.map +1 -1
- package/dist/views/Posts/PostTypesView.d.ts +5 -4
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +8 -0
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/page-editor/PagePreview.d.ts.map +1 -1
- package/dist/views/page-editor/PagePreview.js +10 -1
- package/dist/views/page-editor/PagePreview.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +22 -2
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts +6 -2
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.js +8 -8
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +7 -1
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.js +10 -4
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
- package/dist/views/post-editor/PostHeader.d.ts.map +1 -1
- package/dist/views/post-editor/PostHeader.js +4 -3
- package/dist/views/post-editor/PostHeader.js.map +1 -1
- package/dist/views/post-editor/PostPreview.d.ts.map +1 -1
- package/dist/views/post-editor/PostPreview.js +11 -1
- package/dist/views/post-editor/PostPreview.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts +22 -2
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +4 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/lib/page-editor-service-api.test.ts +180 -0
- package/src/__tests__/lib/pages-service-fetch.test.ts +160 -0
- package/src/__tests__/lib/post-editor-service.test.ts +78 -23
- package/src/__tests__/lib/posts-service.test.ts +134 -163
- package/src/__tests__/views/page-section-editor.test.tsx +65 -0
- package/src/__tests__/views/pages-list-view.test.tsx +119 -0
- package/src/__tests__/views/posts-list-view.render.test.tsx +84 -0
- package/src/index.ts +0 -2
- package/src/lib/page-editor-service.ts +61 -0
- package/src/lib/pages-service.ts +88 -35
- package/src/lib/post-editor-service.ts +41 -40
- package/src/lib/posts-service.ts +109 -180
- package/src/views/Pages/PagesListView.tsx +113 -82
- package/src/views/Posts/NewPostPicker.tsx +5 -4
- package/src/views/Posts/PostTypesView.tsx +5 -4
- package/src/views/Posts/PostsListView.tsx +9 -0
- package/src/views/page-editor/PagePreview.tsx +10 -1
- package/src/views/page-editor/PageSectionEditor.tsx +25 -1
- package/src/views/page-editor/sections/ArticleBodySection.tsx +12 -16
- package/src/views/post-editor/PostEditorCanvas.tsx +7 -1
- package/src/views/post-editor/PostFieldsModal.tsx +36 -21
- package/src/views/post-editor/PostHeader.tsx +4 -4
- package/src/views/post-editor/PostPreview.tsx +12 -1
- package/src/views/post-editor/PostSectionEditor.tsx +22 -5
- package/dist/views/PostEditor.d.ts +0 -6
- package/dist/views/PostEditor.d.ts.map +0 -1
- package/dist/views/PostEditor.js +0 -79
- package/dist/views/PostEditor.js.map +0 -1
- package/dist/views/Posts.d.ts +0 -5
- package/dist/views/Posts.d.ts.map +0 -1
- package/dist/views/Posts.js +0 -73
- package/dist/views/Posts.js.map +0 -1
- package/src/views/PostEditor.tsx +0 -257
- package/src/views/Posts.tsx +0 -375
package/src/views/Posts.tsx
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Plus,
|
|
5
|
-
Search,
|
|
6
|
-
Trash2,
|
|
7
|
-
SlidersHorizontal,
|
|
8
|
-
Pencil,
|
|
9
|
-
ArrowUpDown,
|
|
10
|
-
ArrowUp,
|
|
11
|
-
ArrowDown,
|
|
12
|
-
Loader2,
|
|
13
|
-
AlertTriangle,
|
|
14
|
-
} from 'lucide-react'
|
|
15
|
-
import { useState, useMemo } from 'react'
|
|
16
|
-
import { toast } from 'sonner'
|
|
17
|
-
import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
|
|
18
|
-
import { useApiData } from '../lib/useApiData.js'
|
|
19
|
-
import { cmsApi } from '../lib/api.js'
|
|
20
|
-
|
|
21
|
-
type PostSortKey = 'title' | 'author' | 'category' | 'status' | 'date'
|
|
22
|
-
|
|
23
|
-
export interface PostsProps {
|
|
24
|
-
onNavigate?: (path: string) => void
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function Posts({ onNavigate }: PostsProps) {
|
|
28
|
-
const { data, loading, error, refetch } = useApiData<{ docs: any[]; total: number }>(
|
|
29
|
-
'/collections/posts?pageSize=100',
|
|
30
|
-
)
|
|
31
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
32
|
-
const [filterStatus, setFilterStatus] = useState<string>('all')
|
|
33
|
-
const [filterCategory, setFilterCategory] = useState<string>('all')
|
|
34
|
-
const [selectedPosts, setSelectedPosts] = useState<number[]>([])
|
|
35
|
-
const [sortConfig, setSortConfig] = useState<SortConfig<PostSortKey> | null>(null)
|
|
36
|
-
|
|
37
|
-
const posts = data?.docs ?? []
|
|
38
|
-
|
|
39
|
-
const filteredAndSorted = useMemo(() => {
|
|
40
|
-
let results = posts.filter((post: any) => {
|
|
41
|
-
const matchesSearch =
|
|
42
|
-
(post.title ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
43
|
-
(post.author ?? '').toLowerCase().includes(searchQuery.toLowerCase())
|
|
44
|
-
const matchesStatus =
|
|
45
|
-
filterStatus === 'all' || (post.status ?? '').toLowerCase() === filterStatus.toLowerCase()
|
|
46
|
-
const matchesCategory =
|
|
47
|
-
filterCategory === 'all' ||
|
|
48
|
-
(post.category ?? '').toLowerCase() === filterCategory.toLowerCase()
|
|
49
|
-
return matchesSearch && matchesStatus && matchesCategory
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
if (searchQuery.trim()) {
|
|
53
|
-
results = sortByRelevance(results, searchQuery, (p: any) => [
|
|
54
|
-
p.title,
|
|
55
|
-
p.author ?? '',
|
|
56
|
-
p.category ?? '',
|
|
57
|
-
])
|
|
58
|
-
} else if (sortConfig) {
|
|
59
|
-
results = [...results].sort((a: any, b: any) => {
|
|
60
|
-
const aVal = a[sortConfig.key] ?? ''
|
|
61
|
-
const bVal = b[sortConfig.key] ?? ''
|
|
62
|
-
const cmp = String(aVal).localeCompare(String(bVal))
|
|
63
|
-
return sortConfig.direction === 'asc' ? cmp : -cmp
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
return results
|
|
67
|
-
}, [posts, searchQuery, filterStatus, filterCategory, sortConfig])
|
|
68
|
-
|
|
69
|
-
const handleSelectAll = (checked: boolean) => {
|
|
70
|
-
setSelectedPosts(checked ? filteredAndSorted.map((p: any) => p.id) : [])
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const handleSelectPost = (id: number) => {
|
|
74
|
-
setSelectedPosts((prev) =>
|
|
75
|
-
prev.includes(id) ? prev.filter((pid) => pid !== id) : [...prev, id],
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const handleBulkDelete = async () => {
|
|
80
|
-
for (const id of selectedPosts) {
|
|
81
|
-
await cmsApi(`/collections/posts/${id}`, { method: 'DELETE' })
|
|
82
|
-
}
|
|
83
|
-
toast.success(`${selectedPosts.length} posts deleted`)
|
|
84
|
-
setSelectedPosts([])
|
|
85
|
-
refetch()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const handleDelete = async (id: number) => {
|
|
89
|
-
await cmsApi(`/collections/posts/${id}`, { method: 'DELETE' })
|
|
90
|
-
toast.success('Post deleted')
|
|
91
|
-
setSelectedPosts((prev) => prev.filter((pid) => pid !== id))
|
|
92
|
-
refetch()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function SortHeader({ label, sortKey }: { label: string; sortKey: PostSortKey }) {
|
|
96
|
-
const active = sortConfig?.key === sortKey
|
|
97
|
-
return (
|
|
98
|
-
<button
|
|
99
|
-
type="button"
|
|
100
|
-
onClick={() => setSortConfig(toggleSort(sortConfig, sortKey))}
|
|
101
|
-
className="flex items-center gap-1 text-xs font-medium text-gray-700 transition-colors hover:text-gray-900"
|
|
102
|
-
>
|
|
103
|
-
{label}
|
|
104
|
-
{active ? (
|
|
105
|
-
sortConfig!.direction === 'asc' ? (
|
|
106
|
-
<ArrowUp className="h-3 w-3" />
|
|
107
|
-
) : (
|
|
108
|
-
<ArrowDown className="h-3 w-3" />
|
|
109
|
-
)
|
|
110
|
-
) : (
|
|
111
|
-
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
|
112
|
-
)}
|
|
113
|
-
</button>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (loading) {
|
|
118
|
-
return (
|
|
119
|
-
<div className="flex h-64 items-center justify-center p-3 pr-6 sm:p-4 sm:pr-8">
|
|
120
|
-
<Loader2 className="h-6 w-6 animate-spin text-blue-600" />
|
|
121
|
-
</div>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<div className="flex h-full flex-col p-3 pr-6 sm:p-4 sm:pr-8">
|
|
127
|
-
{error && (
|
|
128
|
-
<div className="mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
129
|
-
<AlertTriangle className="h-5 w-5 shrink-0 text-red-600" />
|
|
130
|
-
<span className="flex-1 text-sm text-red-800">{error}</span>
|
|
131
|
-
<button
|
|
132
|
-
onClick={refetch}
|
|
133
|
-
className="rounded-lg border border-red-300 px-3 py-1 text-sm text-red-700 transition-colors hover:bg-red-100"
|
|
134
|
-
>
|
|
135
|
-
Retry
|
|
136
|
-
</button>
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
<div className="mb-4 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
|
|
141
|
-
<div>
|
|
142
|
-
<h1 className="mb-1 text-xl font-semibold text-gray-900 sm:text-2xl">Posts</h1>
|
|
143
|
-
<p className="text-sm text-gray-600">{filteredAndSorted.length} total posts</p>
|
|
144
|
-
</div>
|
|
145
|
-
<button
|
|
146
|
-
onClick={() => onNavigate?.('/posts/new')}
|
|
147
|
-
className="flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
|
|
148
|
-
>
|
|
149
|
-
<Plus className="h-4 w-4" />
|
|
150
|
-
<span className="hidden sm:inline">New Post</span>
|
|
151
|
-
<span className="sm:hidden">New</span>
|
|
152
|
-
</button>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<div className="mb-4 rounded-lg border border-gray-200 bg-white">
|
|
156
|
-
<div className="flex flex-col gap-3 p-3">
|
|
157
|
-
<div className="relative">
|
|
158
|
-
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
159
|
-
<input
|
|
160
|
-
type="text"
|
|
161
|
-
placeholder="Search posts..."
|
|
162
|
-
value={searchQuery}
|
|
163
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
164
|
-
className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
165
|
-
/>
|
|
166
|
-
</div>
|
|
167
|
-
<div className="flex flex-col gap-2 sm:flex-row">
|
|
168
|
-
<select
|
|
169
|
-
value={filterStatus}
|
|
170
|
-
onChange={(e) => setFilterStatus(e.target.value)}
|
|
171
|
-
className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
172
|
-
>
|
|
173
|
-
<option value="all">All Status</option>
|
|
174
|
-
<option value="published">Published</option>
|
|
175
|
-
<option value="draft">Draft</option>
|
|
176
|
-
</select>
|
|
177
|
-
<select
|
|
178
|
-
value={filterCategory}
|
|
179
|
-
onChange={(e) => setFilterCategory(e.target.value)}
|
|
180
|
-
className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
181
|
-
>
|
|
182
|
-
<option value="all">All Categories</option>
|
|
183
|
-
<option value="technology">Technology</option>
|
|
184
|
-
<option value="design">Design</option>
|
|
185
|
-
<option value="business">Business</option>
|
|
186
|
-
</select>
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
className="flex items-center justify-center gap-2 rounded-lg border border-gray-300 px-3 py-2 text-sm transition-colors hover:bg-gray-50"
|
|
190
|
-
>
|
|
191
|
-
<SlidersHorizontal className="h-4 w-4" />
|
|
192
|
-
<span className="hidden sm:inline">More Filters</span>
|
|
193
|
-
</button>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
{selectedPosts.length > 0 && (
|
|
199
|
-
<div className="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
|
200
|
-
<div className="flex flex-col justify-between gap-2 sm:flex-row sm:items-center">
|
|
201
|
-
<span className="text-sm text-blue-900">
|
|
202
|
-
{selectedPosts.length} post{selectedPosts.length !== 1 ? 's' : ''} selected
|
|
203
|
-
</span>
|
|
204
|
-
<div className="flex items-center gap-2">
|
|
205
|
-
<button
|
|
206
|
-
type="button"
|
|
207
|
-
onClick={handleBulkDelete}
|
|
208
|
-
className="flex-1 rounded-lg bg-red-600 px-3 py-1.5 text-sm text-white transition-colors hover:bg-red-700 sm:flex-none"
|
|
209
|
-
>
|
|
210
|
-
Delete
|
|
211
|
-
</button>
|
|
212
|
-
<button
|
|
213
|
-
type="button"
|
|
214
|
-
onClick={() => setSelectedPosts([])}
|
|
215
|
-
className="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm transition-colors hover:bg-gray-50 sm:flex-none"
|
|
216
|
-
>
|
|
217
|
-
Cancel
|
|
218
|
-
</button>
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
)}
|
|
223
|
-
|
|
224
|
-
{filteredAndSorted.length === 0 && !error ? (
|
|
225
|
-
<div className="flex flex-1 flex-col items-center justify-center rounded-lg border border-gray-200 bg-white p-8 text-center">
|
|
226
|
-
<p className="mb-2 text-sm text-gray-500">No posts yet</p>
|
|
227
|
-
<button
|
|
228
|
-
onClick={() => onNavigate?.('/posts/new')}
|
|
229
|
-
className="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
|
|
230
|
-
>
|
|
231
|
-
Create your first post
|
|
232
|
-
</button>
|
|
233
|
-
</div>
|
|
234
|
-
) : (
|
|
235
|
-
<>
|
|
236
|
-
<div className="hidden flex-1 overflow-hidden rounded-lg border border-gray-200 bg-white md:block">
|
|
237
|
-
<div className="h-full overflow-x-auto">
|
|
238
|
-
<table className="w-full">
|
|
239
|
-
<thead className="sticky top-0 border-b border-gray-200 bg-gray-50">
|
|
240
|
-
<tr>
|
|
241
|
-
<th className="w-8 px-3 py-2 text-left">
|
|
242
|
-
<input
|
|
243
|
-
type="checkbox"
|
|
244
|
-
checked={
|
|
245
|
-
selectedPosts.length === filteredAndSorted.length &&
|
|
246
|
-
filteredAndSorted.length > 0
|
|
247
|
-
}
|
|
248
|
-
onChange={(e) => handleSelectAll(e.target.checked)}
|
|
249
|
-
className="rounded border-gray-300"
|
|
250
|
-
/>
|
|
251
|
-
</th>
|
|
252
|
-
<th className="px-3 py-2 text-left">
|
|
253
|
-
<SortHeader label="Title" sortKey="title" />
|
|
254
|
-
</th>
|
|
255
|
-
<th className="px-3 py-2 text-left">
|
|
256
|
-
<SortHeader label="Author" sortKey="author" />
|
|
257
|
-
</th>
|
|
258
|
-
<th className="px-3 py-2 text-left">
|
|
259
|
-
<SortHeader label="Category" sortKey="category" />
|
|
260
|
-
</th>
|
|
261
|
-
<th className="px-3 py-2 text-left">
|
|
262
|
-
<SortHeader label="Status" sortKey="status" />
|
|
263
|
-
</th>
|
|
264
|
-
<th className="px-3 py-2 text-left">
|
|
265
|
-
<SortHeader label="Date" sortKey="date" />
|
|
266
|
-
</th>
|
|
267
|
-
<th className="px-3 py-2 text-left text-xs font-medium text-gray-700">
|
|
268
|
-
Actions
|
|
269
|
-
</th>
|
|
270
|
-
</tr>
|
|
271
|
-
</thead>
|
|
272
|
-
<tbody className="divide-y divide-gray-200">
|
|
273
|
-
{filteredAndSorted.map((post: any) => (
|
|
274
|
-
<tr key={post.id} className="transition-colors hover:bg-gray-50">
|
|
275
|
-
<td className="px-3 py-2">
|
|
276
|
-
<input
|
|
277
|
-
type="checkbox"
|
|
278
|
-
checked={selectedPosts.includes(post.id)}
|
|
279
|
-
onChange={() => handleSelectPost(post.id)}
|
|
280
|
-
className="rounded border-gray-300"
|
|
281
|
-
/>
|
|
282
|
-
</td>
|
|
283
|
-
<td className="px-3 py-2">
|
|
284
|
-
<button
|
|
285
|
-
type="button"
|
|
286
|
-
onClick={() => onNavigate?.(`/posts/${post.id}`)}
|
|
287
|
-
className="text-left text-sm font-medium text-gray-900 hover:text-blue-600"
|
|
288
|
-
>
|
|
289
|
-
{post.title}
|
|
290
|
-
</button>
|
|
291
|
-
</td>
|
|
292
|
-
<td className="px-3 py-2 text-sm text-gray-600">{post.author}</td>
|
|
293
|
-
<td className="px-3 py-2 text-sm text-gray-600">{post.category}</td>
|
|
294
|
-
<td className="px-3 py-2">
|
|
295
|
-
<span
|
|
296
|
-
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${post.status === 'Published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
|
|
297
|
-
>
|
|
298
|
-
{post.status}
|
|
299
|
-
</span>
|
|
300
|
-
</td>
|
|
301
|
-
<td className="px-3 py-2 text-sm text-gray-600">{post.date}</td>
|
|
302
|
-
<td className="px-3 py-2">
|
|
303
|
-
<div className="flex items-center gap-2">
|
|
304
|
-
<button
|
|
305
|
-
type="button"
|
|
306
|
-
onClick={() => onNavigate?.(`/posts/${post.id}`)}
|
|
307
|
-
className="rounded p-1.5 transition-colors hover:bg-gray-100"
|
|
308
|
-
title="Edit"
|
|
309
|
-
>
|
|
310
|
-
<Pencil className="h-4 w-4 text-gray-600" />
|
|
311
|
-
</button>
|
|
312
|
-
<button
|
|
313
|
-
type="button"
|
|
314
|
-
onClick={() => handleDelete(post.id)}
|
|
315
|
-
className="rounded p-1.5 transition-colors hover:bg-gray-100"
|
|
316
|
-
title="Delete"
|
|
317
|
-
>
|
|
318
|
-
<Trash2 className="h-4 w-4 text-red-600" />
|
|
319
|
-
</button>
|
|
320
|
-
</div>
|
|
321
|
-
</td>
|
|
322
|
-
</tr>
|
|
323
|
-
))}
|
|
324
|
-
</tbody>
|
|
325
|
-
</table>
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
|
|
329
|
-
<div className="flex-1 overflow-auto rounded-lg border border-gray-200 bg-white md:hidden">
|
|
330
|
-
<div className="divide-y divide-gray-200">
|
|
331
|
-
{filteredAndSorted.map((post: any) => (
|
|
332
|
-
<div key={post.id} className="p-3">
|
|
333
|
-
<div className="flex items-start gap-3">
|
|
334
|
-
<input
|
|
335
|
-
type="checkbox"
|
|
336
|
-
checked={selectedPosts.includes(post.id)}
|
|
337
|
-
onChange={() => handleSelectPost(post.id)}
|
|
338
|
-
className="mt-1 rounded border-gray-300"
|
|
339
|
-
/>
|
|
340
|
-
<div className="min-w-0 flex-1">
|
|
341
|
-
<button
|
|
342
|
-
type="button"
|
|
343
|
-
onClick={() => onNavigate?.(`/posts/${post.id}`)}
|
|
344
|
-
className="mb-1 block text-left text-sm font-medium text-gray-900 hover:text-blue-600"
|
|
345
|
-
>
|
|
346
|
-
{post.title}
|
|
347
|
-
</button>
|
|
348
|
-
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-gray-600">
|
|
349
|
-
<span>{post.author}</span>
|
|
350
|
-
<span>•</span>
|
|
351
|
-
<span>{post.date}</span>
|
|
352
|
-
<span
|
|
353
|
-
className={`rounded-full px-2 py-0.5 text-xs font-medium ${post.status === 'Published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
|
|
354
|
-
>
|
|
355
|
-
{post.status}
|
|
356
|
-
</span>
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
onClick={() => handleDelete(post.id)}
|
|
362
|
-
className="rounded p-1.5 transition-colors hover:bg-gray-100"
|
|
363
|
-
>
|
|
364
|
-
<Trash2 className="h-4 w-4 text-red-600" />
|
|
365
|
-
</button>
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
))}
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
</>
|
|
372
|
-
)}
|
|
373
|
-
</div>
|
|
374
|
-
)
|
|
375
|
-
}
|