223 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|