Frontend update
This commit is contained in:
108
client/src/routes/admin/views/PostsAdmin.jsx
Normal file
108
client/src/routes/admin/views/PostsAdmin.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Loading, ErrorState } from '../../../components/PageState.jsx'
|
||||
import { useAsync } from '../../../lib/useAsync.js'
|
||||
import { shortDate, categoryLabel } from '../../../lib/format.js'
|
||||
import { api } from '../../../api/client.js'
|
||||
import PostEditor from './PostEditor.jsx'
|
||||
|
||||
const FILTERS = [
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'news', label: 'News', db: 'news' },
|
||||
{ key: 'five_on_friday', label: 'Five on Friday', db: 'five_on_friday' },
|
||||
{ key: 'newsletter', label: 'Newsletter', db: 'newsletter' },
|
||||
{ key: 'screenshot', label: 'Screenshots', db: 'screenshot' },
|
||||
]
|
||||
|
||||
export default function PostsAdmin() {
|
||||
const [tick, setTick] = useState(0)
|
||||
const reload = useCallback(() => setTick((t) => t + 1), [])
|
||||
const { loading, error, data } = useAsync(() => api.admin.listPosts(), [tick])
|
||||
const [filter, setFilter] = useState('all')
|
||||
const [editing, setEditing] = useState(null) // null | 'new' | post object
|
||||
|
||||
const posts = (data || []).filter((p) => filter === 'all' || p.category === filter)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14, marginBottom: 18, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{FILTERS.map((f) => (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => setFilter(f.key)}
|
||||
className="pill"
|
||||
style={
|
||||
filter === f.key
|
||||
? { borderColor: 'var(--accent)', background: 'var(--blue)', color: 'var(--ink)' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={() => setEditing('new')} className="btn btn-primary btn-sq">
|
||||
+ New post
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && <Loading />}
|
||||
{error && <ErrorState message="Could not load posts." />}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="panel-flat">
|
||||
<table className="adm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="adm-th">Title</th>
|
||||
<th className="adm-th">Category</th>
|
||||
<th className="adm-th">Status</th>
|
||||
<th className="adm-th">Date</th>
|
||||
<th className="adm-th" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{posts.length === 0 && (
|
||||
<tr>
|
||||
<td className="adm-td" colSpan={5} style={{ color: 'var(--muted)' }}>
|
||||
No posts in this category yet.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{posts.map((p) => (
|
||||
<tr key={p.id}>
|
||||
<td className="adm-td" style={{ color: 'var(--head)' }}>
|
||||
{p.title}
|
||||
</td>
|
||||
<td className="adm-td">{categoryLabel(p.category)}</td>
|
||||
<td className="adm-td">
|
||||
<span className={`badge ${p.published ? 'badge-pub' : 'badge-draft'}`}>
|
||||
{p.published ? 'Published' : 'Draft'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="adm-td dim">{shortDate(p.published_at || p.created_at)}</td>
|
||||
<td className="adm-td" style={{ textAlign: 'right' }}>
|
||||
<span className="link-accent" onClick={() => setEditing(p)}>
|
||||
Edit
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<PostEditor
|
||||
post={editing === 'new' ? null : editing}
|
||||
onClose={() => setEditing(null)}
|
||||
onSaved={() => {
|
||||
setEditing(null)
|
||||
reload()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user