545 lines
22 KiB
TypeScript
545 lines
22 KiB
TypeScript
|
|
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
||
|
|
import { A } from '@solidjs/router';
|
||
|
|
import AdminShell from '~/components/AdminShell';
|
||
|
|
|
||
|
|
const API = '/api/gateway';
|
||
|
|
|
||
|
|
type KbCategory = {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
slug: string;
|
||
|
|
description?: string;
|
||
|
|
article_count?: number;
|
||
|
|
updated_at?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
type KbArticle = {
|
||
|
|
id: string;
|
||
|
|
title: string;
|
||
|
|
slug?: string;
|
||
|
|
category_id?: string;
|
||
|
|
category?: string;
|
||
|
|
status: string;
|
||
|
|
updated_at?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
async function loadCategories(): Promise<KbCategory[]> {
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/api/admin/kb/categories`);
|
||
|
|
if (!res.ok) throw new Error('Failed to load');
|
||
|
|
const data = await res.json();
|
||
|
|
return Array.isArray(data) ? data : (data.categories || []);
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadArticles(): Promise<KbArticle[]> {
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/api/admin/kb/articles`);
|
||
|
|
if (!res.ok) throw new Error('Failed to load');
|
||
|
|
const data = await res.json();
|
||
|
|
return Array.isArray(data) ? data : (data.articles || []);
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function KbPage() {
|
||
|
|
const [tab, setTab] = createSignal<'categories' | 'articles' | 'create-article'>('categories');
|
||
|
|
|
||
|
|
// Categories resource
|
||
|
|
const [categories, { refetch: refetchCategories }] = createResource(loadCategories);
|
||
|
|
// Articles resource
|
||
|
|
const [articles, { refetch: refetchArticles }] = createResource(loadArticles);
|
||
|
|
|
||
|
|
// --- Categories tab state ---
|
||
|
|
const [showCatForm, setShowCatForm] = createSignal(false);
|
||
|
|
const [catName, setCatName] = createSignal('');
|
||
|
|
const [catSlug, setCatSlug] = createSignal('');
|
||
|
|
const [catDesc, setCatDesc] = createSignal('');
|
||
|
|
const [catSaving, setCatSaving] = createSignal(false);
|
||
|
|
const [catError, setCatError] = createSignal('');
|
||
|
|
const [editingCatId, setEditingCatId] = createSignal('');
|
||
|
|
const [editCatName, setEditCatName] = createSignal('');
|
||
|
|
const [editCatSlug, setEditCatSlug] = createSignal('');
|
||
|
|
const [editCatSaving, setEditCatSaving] = createSignal(false);
|
||
|
|
const [editCatError, setEditCatError] = createSignal('');
|
||
|
|
const [deletingCatId, setDeletingCatId] = createSignal('');
|
||
|
|
const [actionError, setActionError] = createSignal('');
|
||
|
|
|
||
|
|
// --- Articles tab state ---
|
||
|
|
const [articleSearch, setArticleSearch] = createSignal('');
|
||
|
|
const [deletingArticleId, setDeletingArticleId] = createSignal('');
|
||
|
|
const [articleActionError, setArticleActionError] = createSignal('');
|
||
|
|
|
||
|
|
// --- Create Article tab state ---
|
||
|
|
const [artTitle, setArtTitle] = createSignal('');
|
||
|
|
const [artSlug, setArtSlug] = createSignal('');
|
||
|
|
const [artCategoryId, setArtCategoryId] = createSignal('');
|
||
|
|
const [artContent, setArtContent] = createSignal('');
|
||
|
|
const [artStatus, setArtStatus] = createSignal('DRAFT');
|
||
|
|
const [artSaving, setArtSaving] = createSignal(false);
|
||
|
|
const [artError, setArtError] = createSignal('');
|
||
|
|
|
||
|
|
// Filtered articles
|
||
|
|
const filteredArticles = createMemo(() => {
|
||
|
|
const all = articles() ?? [];
|
||
|
|
const q = articleSearch().toLowerCase();
|
||
|
|
if (!q) return all;
|
||
|
|
return all.filter((a) => a.title?.toLowerCase().includes(q));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Categories actions
|
||
|
|
const handleAddCategory = async (e: Event) => {
|
||
|
|
e.preventDefault();
|
||
|
|
try {
|
||
|
|
setCatSaving(true);
|
||
|
|
setCatError('');
|
||
|
|
const res = await fetch(`${API}/api/admin/kb/categories`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ name: catName(), slug: catSlug(), description: catDesc() }),
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error('Failed to create category');
|
||
|
|
setCatName('');
|
||
|
|
setCatSlug('');
|
||
|
|
setCatDesc('');
|
||
|
|
setShowCatForm(false);
|
||
|
|
refetchCategories();
|
||
|
|
} catch (err: any) {
|
||
|
|
setCatError(err.message || 'Failed to create');
|
||
|
|
} finally {
|
||
|
|
setCatSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const startEditCat = (cat: KbCategory) => {
|
||
|
|
setEditingCatId(cat.id);
|
||
|
|
setEditCatName(cat.name);
|
||
|
|
setEditCatSlug(cat.slug);
|
||
|
|
setEditCatError('');
|
||
|
|
};
|
||
|
|
|
||
|
|
const cancelEditCat = () => {
|
||
|
|
setEditingCatId('');
|
||
|
|
setEditCatError('');
|
||
|
|
};
|
||
|
|
|
||
|
|
const saveEditCat = async (id: string) => {
|
||
|
|
try {
|
||
|
|
setEditCatSaving(true);
|
||
|
|
setEditCatError('');
|
||
|
|
const res = await fetch(`${API}/api/admin/kb/categories/${id}`, {
|
||
|
|
method: 'PATCH',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ name: editCatName(), slug: editCatSlug() }),
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error('Failed to save');
|
||
|
|
setEditingCatId('');
|
||
|
|
refetchCategories();
|
||
|
|
} catch (err: any) {
|
||
|
|
setEditCatError(err.message || 'Failed to save');
|
||
|
|
} finally {
|
||
|
|
setEditCatSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const deleteCategory = async (id: string, name: string) => {
|
||
|
|
if (!confirm(`Delete category "${name}"?`)) return;
|
||
|
|
try {
|
||
|
|
setDeletingCatId(id);
|
||
|
|
setActionError('');
|
||
|
|
const res = await fetch(`${API}/api/admin/kb/categories/${id}`, { method: 'DELETE' });
|
||
|
|
if (!res.ok) throw new Error('Failed to delete');
|
||
|
|
refetchCategories();
|
||
|
|
} catch (err: any) {
|
||
|
|
setActionError(err.message || 'Failed to delete');
|
||
|
|
} finally {
|
||
|
|
setDeletingCatId('');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Articles actions
|
||
|
|
const deleteArticle = async (id: string, title: string) => {
|
||
|
|
if (!confirm(`Delete article "${title}"?`)) return;
|
||
|
|
try {
|
||
|
|
setDeletingArticleId(id);
|
||
|
|
setArticleActionError('');
|
||
|
|
const res = await fetch(`${API}/api/admin/kb/articles/${id}`, { method: 'DELETE' });
|
||
|
|
if (!res.ok) throw new Error('Failed to delete');
|
||
|
|
refetchArticles();
|
||
|
|
} catch (err: any) {
|
||
|
|
setArticleActionError(err.message || 'Failed to delete');
|
||
|
|
} finally {
|
||
|
|
setDeletingArticleId('');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Create article
|
||
|
|
const handleCreateArticle = async (e: Event) => {
|
||
|
|
e.preventDefault();
|
||
|
|
try {
|
||
|
|
setArtSaving(true);
|
||
|
|
setArtError('');
|
||
|
|
const body: Record<string, any> = {
|
||
|
|
title: artTitle(),
|
||
|
|
slug: artSlug(),
|
||
|
|
content: artContent(),
|
||
|
|
status: artStatus(),
|
||
|
|
};
|
||
|
|
if (artCategoryId()) body.category_id = artCategoryId();
|
||
|
|
const res = await fetch(`${API}/api/admin/kb/articles`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(body),
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error('Failed to create article');
|
||
|
|
setArtTitle('');
|
||
|
|
setArtSlug('');
|
||
|
|
setArtCategoryId('');
|
||
|
|
setArtContent('');
|
||
|
|
setArtStatus('DRAFT');
|
||
|
|
setTab('articles');
|
||
|
|
refetchArticles();
|
||
|
|
} catch (err: any) {
|
||
|
|
setArtError(err.message || 'Failed to create');
|
||
|
|
} finally {
|
||
|
|
setArtSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AdminShell>
|
||
|
|
<div class="page-actions">
|
||
|
|
<div>
|
||
|
|
<h1 class="page-title">Knowledge Base</h1>
|
||
|
|
<p class="page-subtitle">Manage help articles and categories</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Tabs */}
|
||
|
|
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
class={`admin-tab${tab() === 'categories' ? ' active' : ''}`}
|
||
|
|
onClick={() => setTab('categories')}
|
||
|
|
>
|
||
|
|
Categories
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
class={`admin-tab${tab() === 'articles' ? ' active' : ''}`}
|
||
|
|
onClick={() => setTab('articles')}
|
||
|
|
>
|
||
|
|
Articles
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
class={`admin-tab${tab() === 'create-article' ? ' active' : ''}`}
|
||
|
|
onClick={() => setTab('create-article')}
|
||
|
|
>
|
||
|
|
Create Article
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Categories Tab */}
|
||
|
|
<Show when={tab() === 'categories'}>
|
||
|
|
<div class="page-actions" style="margin-bottom:16px">
|
||
|
|
<div />
|
||
|
|
<button class="btn navy" onClick={() => setShowCatForm(!showCatForm())}>
|
||
|
|
{showCatForm() ? 'Cancel' : 'Add Category'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Show when={actionError()}>
|
||
|
|
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<Show when={showCatForm()}>
|
||
|
|
<section class="card" style="margin-bottom:16px;max-width:480px">
|
||
|
|
<h2 style="margin:0 0 16px;font-size:15px;font-weight:700">New Category</h2>
|
||
|
|
<Show when={catError()}>
|
||
|
|
<div class="error-box" style="margin-bottom:10px">{catError()}</div>
|
||
|
|
</Show>
|
||
|
|
<form onSubmit={handleAddCategory} style="display:flex;flex-direction:column;gap:12px">
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={catName()}
|
||
|
|
onInput={(e) => setCatName(e.currentTarget.value)}
|
||
|
|
required
|
||
|
|
placeholder="e.g. Getting Started"
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Slug</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={catSlug()}
|
||
|
|
onInput={(e) => setCatSlug(e.currentTarget.value)}
|
||
|
|
required
|
||
|
|
placeholder="e.g. getting-started"
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Description</label>
|
||
|
|
<textarea
|
||
|
|
value={catDesc()}
|
||
|
|
onInput={(e) => setCatDesc(e.currentTarget.value)}
|
||
|
|
rows="3"
|
||
|
|
placeholder="Brief description..."
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<button class="btn navy" type="submit" disabled={catSaving()}>
|
||
|
|
{catSaving() ? 'Saving...' : 'Create Category'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</section>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<section class="card" style="padding:0;overflow:hidden">
|
||
|
|
<div class="table-wrap">
|
||
|
|
<table class="list-table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Name</th>
|
||
|
|
<th>Slug</th>
|
||
|
|
<th>Article Count</th>
|
||
|
|
<th class="align-right">Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<Show when={categories.loading}>
|
||
|
|
<tr><td colspan="4" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||
|
|
</Show>
|
||
|
|
<Show when={!categories.loading && categories.error}>
|
||
|
|
<tr><td colspan="4" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||
|
|
</Show>
|
||
|
|
<Show when={!categories.loading && !categories.error && (categories()?.length ?? 0) === 0}>
|
||
|
|
<tr><td colspan="4" style="text-align:center;padding:32px;color:#94a3b8">No categories found.</td></tr>
|
||
|
|
</Show>
|
||
|
|
<Show when={!categories.loading && !categories.error && (categories()?.length ?? 0) > 0}>
|
||
|
|
<For each={categories()}>
|
||
|
|
{(cat) => (
|
||
|
|
<>
|
||
|
|
<tr>
|
||
|
|
<td style="font-weight:600;color:#0f172a">{cat.name}</td>
|
||
|
|
<td style="color:#475569;font-family:monospace;font-size:13px">{cat.slug}</td>
|
||
|
|
<td style="color:#475569">{cat.article_count ?? '—'}</td>
|
||
|
|
<td>
|
||
|
|
<div class="table-actions">
|
||
|
|
<button class="btn" onClick={() => startEditCat(cat)}>Edit</button>
|
||
|
|
<button
|
||
|
|
class="btn danger"
|
||
|
|
disabled={deletingCatId() === cat.id}
|
||
|
|
onClick={() => deleteCategory(cat.id, cat.name)}
|
||
|
|
>
|
||
|
|
{deletingCatId() === cat.id ? '...' : 'Delete'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<Show when={editingCatId() === cat.id}>
|
||
|
|
<tr>
|
||
|
|
<td colspan="4" style="background:#f8fafc;padding:14px">
|
||
|
|
<Show when={editCatError()}>
|
||
|
|
<div class="error-box" style="margin-bottom:8px">{editCatError()}</div>
|
||
|
|
</Show>
|
||
|
|
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={editCatName()}
|
||
|
|
onInput={(e) => setEditCatName(e.currentTarget.value)}
|
||
|
|
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Slug</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={editCatSlug()}
|
||
|
|
onInput={(e) => setEditCatSlug(e.currentTarget.value)}
|
||
|
|
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div style="display:flex;gap:8px">
|
||
|
|
<button class="btn navy" disabled={editCatSaving()} onClick={() => saveEditCat(cat.id)}>
|
||
|
|
{editCatSaving() ? 'Saving...' : 'Save'}
|
||
|
|
</button>
|
||
|
|
<button class="btn" onClick={cancelEditCat}>Cancel</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</Show>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</For>
|
||
|
|
</Show>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
{/* Articles Tab */}
|
||
|
|
<Show when={tab() === 'articles'}>
|
||
|
|
<Show when={articleActionError()}>
|
||
|
|
<div class="error-box" style="margin-bottom:12px">{articleActionError()}</div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<div style="margin-bottom:16px">
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="Search articles by title..."
|
||
|
|
value={articleSearch()}
|
||
|
|
onInput={(e) => setArticleSearch(e.currentTarget.value)}
|
||
|
|
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:280px"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<section class="card" style="padding:0;overflow:hidden">
|
||
|
|
<div class="table-wrap">
|
||
|
|
<table class="list-table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Title</th>
|
||
|
|
<th>Category</th>
|
||
|
|
<th>Status</th>
|
||
|
|
<th>Updated At</th>
|
||
|
|
<th class="align-right">Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<Show when={articles.loading}>
|
||
|
|
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||
|
|
</Show>
|
||
|
|
<Show when={!articles.loading && articles.error}>
|
||
|
|
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||
|
|
</Show>
|
||
|
|
<Show when={!articles.loading && !articles.error && filteredArticles().length === 0}>
|
||
|
|
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No articles found.</td></tr>
|
||
|
|
</Show>
|
||
|
|
<Show when={!articles.loading && !articles.error && filteredArticles().length > 0}>
|
||
|
|
<For each={filteredArticles()}>
|
||
|
|
{(article) => (
|
||
|
|
<tr>
|
||
|
|
<td style="font-weight:600;color:#0f172a">{article.title}</td>
|
||
|
|
<td style="color:#475569">{article.category || '—'}</td>
|
||
|
|
<td>
|
||
|
|
<span class={`status-chip ${article.status === 'PUBLISHED' ? 'active' : 'draft'}`}>
|
||
|
|
{article.status || '—'}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td style="color:#475569">
|
||
|
|
{article.updated_at ? new Date(article.updated_at).toLocaleString() : '—'}
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<div class="table-actions">
|
||
|
|
<A class="btn" href={`/admin/kb/articles/${article.id}/edit`}>Edit</A>
|
||
|
|
<button
|
||
|
|
class="btn danger"
|
||
|
|
disabled={deletingArticleId() === article.id}
|
||
|
|
onClick={() => deleteArticle(article.id, article.title)}
|
||
|
|
>
|
||
|
|
{deletingArticleId() === article.id ? '...' : 'Delete'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
)}
|
||
|
|
</For>
|
||
|
|
</Show>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
{/* Create Article Tab */}
|
||
|
|
<Show when={tab() === 'create-article'}>
|
||
|
|
<section class="card" style="max-width:640px">
|
||
|
|
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Article</h2>
|
||
|
|
<Show when={artError()}>
|
||
|
|
<div class="error-box" style="margin-bottom:14px">{artError()}</div>
|
||
|
|
</Show>
|
||
|
|
<form onSubmit={handleCreateArticle} style="display:flex;flex-direction:column;gap:14px">
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Title</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={artTitle()}
|
||
|
|
onInput={(e) => setArtTitle(e.currentTarget.value)}
|
||
|
|
required
|
||
|
|
placeholder="e.g. How to reset your password"
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Slug</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={artSlug()}
|
||
|
|
onInput={(e) => setArtSlug(e.currentTarget.value)}
|
||
|
|
placeholder="e.g. how-to-reset-password"
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Category</label>
|
||
|
|
<select
|
||
|
|
value={artCategoryId()}
|
||
|
|
onChange={(e) => setArtCategoryId(e.currentTarget.value)}
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||
|
|
>
|
||
|
|
<option value="">— Select category —</option>
|
||
|
|
<Show when={!categories.loading}>
|
||
|
|
<For each={categories() ?? []}>
|
||
|
|
{(cat) => <option value={cat.id}>{cat.name}</option>}
|
||
|
|
</For>
|
||
|
|
</Show>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Content</label>
|
||
|
|
<textarea
|
||
|
|
value={artContent()}
|
||
|
|
onInput={(e) => setArtContent(e.currentTarget.value)}
|
||
|
|
required
|
||
|
|
rows="12"
|
||
|
|
placeholder="Write article content here..."
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Status</label>
|
||
|
|
<select
|
||
|
|
value={artStatus()}
|
||
|
|
onChange={(e) => setArtStatus(e.currentTarget.value)}
|
||
|
|
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||
|
|
>
|
||
|
|
<option value="DRAFT">DRAFT</option>
|
||
|
|
<option value="PUBLISHED">PUBLISHED</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<button class="btn navy" type="submit" disabled={artSaving()}>
|
||
|
|
{artSaving() ? 'Creating...' : 'Create Article'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</section>
|
||
|
|
</Show>
|
||
|
|
</AdminShell>
|
||
|
|
);
|
||
|
|
}
|