109 lines
3.8 KiB
JavaScript
109 lines
3.8 KiB
JavaScript
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>
|
|
)
|
|
}
|