Files
soroushasadi/components/admin/PostEditor.tsx
T
soroush.asadi add78d8460
ci / build (push) Failing after 23s
deploy / deploy (push) Failing after 10m12s
first commit
2026-05-31 12:47:02 +03:30

166 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { JsonForm, type JsonValue } from './JsonForm';
type Status = 'idle' | 'saving' | 'saved' | 'error';
type Tab = 'meta' | 'fa' | 'en';
/**
* Edits a single blog article body. Top-level `date` / `accent` live in the
* "Meta" tab; the long-form FA and EN articles each get their own tab so the
* Persian body renders RTL. Saving stores the whole PostContent under the
* article's slug via /api/admin/posts.
*/
export function PostEditor({
slug,
title,
initial,
isOverridden,
}: {
slug: string;
title: string;
initial: { date: JsonValue; accent: JsonValue; fa: JsonValue; en: JsonValue };
isOverridden: boolean;
}) {
const router = useRouter();
const [data, setData] = useState(initial);
const [tab, setTab] = useState<Tab>('meta');
const [status, setStatus] = useState<Status>('idle');
const [overridden, setOverridden] = useState(isOverridden);
async function save() {
setStatus('saving');
try {
const payload = {
date: data.date,
accent: data.accent,
en: data.en,
fa: data.fa,
};
const res = await fetch('/api/admin/posts', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ slug, data: payload }),
});
if (!res.ok) throw new Error(String(res.status));
setStatus('saved');
setOverridden(true);
router.refresh();
setTimeout(() => setStatus('idle'), 2500);
} catch {
setStatus('error');
}
}
async function reset() {
if (!confirm('Revert this article to its built-in default? Your edits will be removed.')) return;
setStatus('saving');
try {
const res = await fetch(`/api/admin/posts?slug=${encodeURIComponent(slug)}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error(String(res.status));
router.refresh();
window.location.reload();
} catch {
setStatus('error');
}
}
return (
<div className="flex flex-col gap-6">
{/* Toolbar */}
<div className="sticky top-0 z-10 -mx-6 flex flex-wrap items-center justify-between gap-3 border-b border-white/8 bg-base-900/80 px-6 py-3 backdrop-blur sm:-mx-8 sm:px-8">
<div className="flex items-center gap-1 rounded-full border border-white/10 bg-white/[0.02] p-1">
<TabBtn active={tab === 'meta'} onClick={() => setTab('meta')}>Meta</TabBtn>
<TabBtn active={tab === 'fa'} onClick={() => setTab('fa')}>FA · فارسی</TabBtn>
<TabBtn active={tab === 'en'} onClick={() => setTab('en')}>EN · English</TabBtn>
</div>
<div className="flex items-center gap-2">
{status === 'saved' && <span className="text-sm text-emerald">Saved </span>}
{status === 'error' && <span className="text-sm text-magenta">Save failed</span>}
{overridden && (
<button
type="button"
onClick={reset}
className="rounded-lg border border-white/10 px-3 py-2 text-sm text-slate-300 transition-colors hover:bg-white/[0.05]"
>
Reset to default
</button>
)}
<button
type="button"
onClick={save}
disabled={status === 'saving'}
className="rounded-lg bg-electric px-4 py-2 text-sm font-semibold text-base-900 transition-opacity hover:opacity-90 disabled:opacity-50"
>
{status === 'saving' ? 'Saving…' : 'Save changes'}
</button>
</div>
</div>
{tab === 'meta' && (
<div className="flex flex-col gap-5 pb-24">
<p className="rounded-lg border border-white/8 bg-white/[0.02] p-3 text-xs text-slate-400">
Editing <span className="font-mono text-electric">{slug}</span>. Accent must be one of:
electric, violet, magenta, emerald, cyan. The card title/excerpt live under the
<span className="font-mono"> Journal</span> section.
</p>
<JsonForm
value={{ date: data.date, accent: data.accent }}
onChange={(nv) => {
const o = nv as { date: JsonValue; accent: JsonValue };
setData((d) => ({ ...d, date: o.date, accent: o.accent }));
}}
/>
</div>
)}
{tab === 'fa' && (
<div dir="rtl" className="pb-24">
<JsonForm
key="fa"
value={data.fa}
onChange={(nv) => setData((d) => ({ ...d, fa: nv }))}
/>
</div>
)}
{tab === 'en' && (
<div dir="ltr" className="pb-24">
<JsonForm
key="en"
value={data.en}
onChange={(nv) => setData((d) => ({ ...d, en: nv }))}
/>
</div>
)}
</div>
);
}
function TabBtn({
active,
onClick,
children,
}: {
active: boolean;
onClick: () => void;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
className={
'rounded-full px-4 py-1.5 text-sm font-medium transition-colors ' +
(active ? 'bg-electric text-base-900' : 'text-slate-300 hover:text-white')
}
>
{children}
</button>
);
}