nxtgauge-admin-solid/src/routes/admin/kb/articles/[id]/edit.tsx

163 lines
5.9 KiB
TypeScript

import { A, useNavigate, useParams } from '@solidjs/router';
import { createEffect, createResource, createSignal, Show } from 'solid-js';
const API = '';
function getToken(): string {
return typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
}
type KbArticle = {
id: string;
title: string;
slug?: string;
content?: string;
body?: string;
status?: string;
category_id?: string;
};
async function loadArticle(id: string): Promise<KbArticle | null> {
try {
const token = getToken();
const res = await fetch(`${API}/api/admin/kb/articles/${id}`, {
headers: {
Accept: 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
credentials: 'include',
});
if (!res.ok) return null;
const data = await res.json();
return {
...data,
content: data?.content ?? data?.body ?? '',
body: data?.body ?? data?.content ?? '',
};
} catch {
return null;
}
}
export default function KbArticleEditPage() {
const navigate = useNavigate();
const params = useParams();
const [article] = createResource(() => params.id, loadArticle);
const [title, setTitle] = createSignal('');
const [slug, setSlug] = createSignal('');
const [categoryId, setCategoryId] = createSignal('');
const [status, setStatus] = createSignal('DRAFT');
const [content, setContent] = createSignal('');
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal('');
const [loaded, setLoaded] = createSignal(false);
createEffect(() => {
const value = article();
if (!value || loaded()) return;
setTitle(value.title || '');
setSlug(value.slug || '');
setCategoryId(value.category_id || '');
setStatus(value.status || 'DRAFT');
setContent(value.content || value.body || '');
setLoaded(true);
});
const save = async (e: Event) => {
e.preventDefault();
try {
setSaving(true);
setError('');
const res = await fetch(`${API}/api/admin/kb/articles/${params.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}),
},
credentials: 'include',
body: JSON.stringify({
title: title(),
slug: slug(),
category_id: categoryId() || null,
status: status(),
content: content(),
}),
});
if (!res.ok) throw new Error('Failed to save article');
navigate(`/admin/kb/articles/${params.id}`);
} catch (err: any) {
setError(err.message || 'Failed to save article');
} finally {
setSaving(false);
}
};
const inputCls = 'w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]';
const labelCls = 'mb-1.5 block text-sm font-medium text-gray-700';
return (
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Edit KB Article</h1>
<p class="text-sm text-gray-500 mt-0.5">Update article metadata, status, and content.</p>
</div>
<div class="flex items-center gap-2">
<A class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/kb/articles/${params.id}`}>Back to Detail</A>
<A class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/kb/articles">Back to Articles</A>
</div>
</div>
<div class="p-6 flex-1">
<Show when={article.loading}>
<div class="table-card"><p class="py-10 text-center text-sm text-slate-400">Loading article</p></div>
</Show>
<Show when={!article.loading && !article()}>
<div class="table-card"><p class="py-10 text-center text-sm text-slate-400">Article not found.</p></div>
</Show>
<Show when={article() && loaded()}>
<form class="rounded-xl border border-gray-200 bg-white shadow-sm p-6" onSubmit={save}>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div>
<label class={labelCls}>Title</label>
<input class={inputCls} value={title()} onInput={(e) => setTitle(e.currentTarget.value)} required />
</div>
<div>
<label class={labelCls}>Slug</label>
<input class={inputCls} value={slug()} onInput={(e) => setSlug(e.currentTarget.value)} />
</div>
<div>
<label class={labelCls}>Category ID</label>
<input class={inputCls} value={categoryId()} onInput={(e) => setCategoryId(e.currentTarget.value)} />
</div>
<div>
<label class={labelCls}>Status</label>
<select class={inputCls} value={status()} onChange={(e) => setStatus(e.currentTarget.value)}>
<option value="DRAFT">DRAFT</option>
<option value="PUBLISHED">PUBLISHED</option>
</select>
</div>
<div class="sm:col-span-2">
<label class={labelCls}>Content</label>
<textarea rows="16" class={inputCls} value={content()} onInput={(e) => setContent(e.currentTarget.value)} />
</div>
</div>
<Show when={error()}>
<p class="mt-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</p>
</Show>
<div class="mt-6 flex justify-end border-t border-gray-100 pt-5">
<button class="btn-primary" type="submit" disabled={saving()}>
{saving() ? 'Saving…' : 'Save Article'}
</button>
</div>
</form>
</Show>
</div>
</div>
);
}