Files
dashboards/components/WebsiteBlogForm.tsx
T

223 lines
6.5 KiB
TypeScript

"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import CategoryTreeSelect from "@/components/CategoryTreeSelect";
import {
FeaturedImageField,
TagsField,
type LocalImage,
} from "@/components/catalog/CatalogFormFields";
import RichTextArea from "@/components/RichTextArea";
import type { MeshkeeCategory } from "@/services/meshkee-categories";
import { uploadWebsiteMedia } from "@/services/website-media";
import {
createWebsiteBlogPost,
listWebsiteBlogCategories,
type WebsiteBlogCategory,
} from "@/services/website-blog";
import styles from "@/components/catalog/CatalogForm.module.css";
import local from "./WebsiteBlogForm.module.css";
export default function WebsiteBlogForm() {
const router = useRouter();
const [categories, setCategories] = useState<WebsiteBlogCategory[]>([]);
const [categoryId, setCategoryId] = useState("");
const [title, setTitle] = useState("");
const [abstract, setAbstract] = useState("");
const [mainTextHtml, setMainTextHtml] = useState("");
const [image, setImage] = useState<LocalImage | null>(null);
const [tags, setTags] = useState<string[]>([]);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const items = await listWebsiteBlogCategories();
if (!cancelled) setCategories(items);
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error ? err.message : "Could not load categories.",
);
}
}
})();
return () => {
cancelled = true;
};
}, []);
async function onSubmit(event?: React.FormEvent) {
event?.preventDefault();
if (!title.trim()) {
setError("Enter a title.");
return;
}
setSaving(true);
setError("");
try {
if (!categoryId) {
setError("Choose a category.");
setSaving(false);
return;
}
let imageUrl: string | undefined;
if (image?.file) {
imageUrl = await uploadWebsiteMedia(image.file);
}
await createWebsiteBlogPost({
title: title.trim(),
categoryId,
abstract: abstract.trim() || undefined,
mainTextHtml: mainTextHtml || undefined,
imageUrl,
tags,
});
router.push("/website/blog");
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Could not create post.");
} finally {
setSaving(false);
}
}
const busy = saving;
const treeCategories: MeshkeeCategory[] = categories.map((item) => ({
id: item.id,
parentId: item.parentId,
name: item.name,
nameFa: null,
slug: "",
description: null,
icon: null,
sortOrder: 0,
tags: [],
}));
return (
<form className={styles.page} onSubmit={onSubmit}>
<header className={styles.head}>
<div>
<nav className={styles.breadcrumb} aria-label="Breadcrumb">
<Link href="/">Home</Link>
<span className={styles.breadcrumbSep} aria-hidden>
/
</span>
<Link href="/website/blog">Blog</Link>
<span className={styles.breadcrumbSep} aria-hidden>
/
</span>
<span className={styles.breadcrumbCurrent}>Add</span>
</nav>
<h1 className={styles.title}>Add blog post</h1>
<p className={styles.lead}>
Publish a story on the IranBay website.
</p>
</div>
<div className={styles.headActions}>
<Link href="/website/blog" className={`outline-btn ${styles.ghostBtn}`}>
Back
</Link>
<button
type="submit"
className={`outline-btn ${styles.saveBtn}`}
disabled={busy}
>
{saving ? "Saving…" : "Save"}
</button>
</div>
</header>
{error ? (
<div className={styles.errorBox} role="alert">
{error}
</div>
) : null}
<div className={styles.layout}>
<section className={styles.card}>
<h2 className={styles.sectionTitle}>Post</h2>
<div className={local.beside}>
<FeaturedImageField
label="Image"
value={image}
aspect={3 / 2}
disabled={busy}
onChange={setImage}
/>
<div className={styles.stack}>
<CategoryTreeSelect
categories={treeCategories}
value={categoryId ? [categoryId] : []}
selection="single"
label="Category"
placeholder="Select category"
disabled={busy}
onChange={(ids) => setCategoryId(ids[0] ?? "")}
/>
<label className={styles.field}>
<span>Title</span>
<input
value={title}
disabled={busy}
required
onChange={(event) => setTitle(event.target.value)}
/>
</label>
<label className={styles.field}>
<span>Abstract</span>
<textarea
rows={3}
value={abstract}
disabled={busy}
onChange={(event) => setAbstract(event.target.value)}
/>
</label>
</div>
</div>
</section>
<section className={styles.card}>
<h2 className={styles.sectionTitle}>Content</h2>
<div className={styles.stack}>
<div className={styles.richField}>
<RichTextArea
label="Main text"
value={mainTextHtml}
onChange={setMainTextHtml}
disabled={busy}
minHeight={280}
onUploadImage={uploadWebsiteMedia}
/>
</div>
<TagsField tags={tags} disabled={busy} onChange={setTags} />
</div>
</section>
<div className={styles.footerActions}>
<Link href="/website/blog" className={`outline-btn ${styles.ghostBtn}`}>
Cancel
</Link>
<button
type="submit"
className={`outline-btn ${styles.saveBtn}`}
disabled={busy}
>
{saving ? "Saving…" : "Save"}
</button>
</div>
</div>
</form>
);
}