Files

259 lines
7.7 KiB
TypeScript

"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import {
FeaturedImageField,
TagsField,
type LocalImage,
} from "@/components/catalog/CatalogFormFields";
import CatalogCategorySelect from "@/components/catalog/CatalogCategorySelect";
import RichTextArea from "@/components/RichTextArea";
import {
createMeshkeeBlog,
listMeshkeeEntityCategories,
uploadMeshkeeMediaFile,
type MeshkeeEntityCategory,
} from "@/services/meshkee-catalog";
import styles from "./CatalogForm.module.css";
const BLOG_TYPES = [
{ value: "blog", label: "Blog" },
{ value: "article", label: "Article" },
{ value: "news", label: "News" },
] as const;
export default function AddBlogForm({
meshkeeId,
listHref,
title,
lead,
}: {
meshkeeId: string;
listHref: string;
title: string;
lead: string;
}) {
const router = useRouter();
const [categories, setCategories] = useState<MeshkeeEntityCategory[]>([]);
const [categoryId, setCategoryId] = useState("");
const [type, setType] = useState<"blog" | "article" | "news">("blog");
const [postTitle, setPostTitle] = useState("");
const [abstract, setAbstract] = useState("");
const [mainTextHtml, setMainTextHtml] = useState("");
const [titleImage, setTitleImage] = useState<LocalImage | null>(null);
const [tags, setTags] = useState<string[]>([]);
const [seoMetaTitle, setSeoMetaTitle] = useState("");
const [seoMetaDescription, setSeoMetaDescription] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const items = await listMeshkeeEntityCategories(meshkeeId, "blog");
if (!cancelled) setCategories(items);
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error ? err.message : "Could not load categories.",
);
}
}
})();
return () => {
cancelled = true;
};
}, [meshkeeId]);
async function onSubmit(event?: React.FormEvent) {
event?.preventDefault();
if (!postTitle.trim()) {
setError("Enter a title.");
return;
}
setSaving(true);
setError("");
try {
let featuredMediaId: string | undefined;
if (titleImage?.file) {
featuredMediaId = (
await uploadMeshkeeMediaFile(meshkeeId, titleImage.file)
).id;
}
await createMeshkeeBlog(meshkeeId, {
title: postTitle.trim(),
type,
abstract: abstract.trim() || undefined,
mainTextHtml: mainTextHtml || undefined,
categoryId: categoryId || undefined,
featuredMediaId,
tags,
seoMetaTitle: seoMetaTitle.trim() || null,
seoMetaDescription: seoMetaDescription.trim() || null,
});
router.push(listHref);
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Could not create blog post.");
} finally {
setSaving(false);
}
}
return (
<form className={styles.page} onSubmit={onSubmit}>
<header className={styles.head}>
<div>
<h1 className={styles.title}>{title}</h1>
<p className={styles.lead}>{lead}</p>
</div>
<div className={styles.headActions}>
<Link href={listHref} className={`outline-btn ${styles.ghostBtn}`}>
Back
</Link>
<button
type="submit"
className={`outline-btn ${styles.saveBtn}`}
disabled={saving}
>
{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}>Media</h2>
<div className={styles.mediaGrid}>
<FeaturedImageField
label="Title image"
value={titleImage}
aspect={3 / 2}
disabled={saving}
onChange={setTitleImage}
/>
<div className={styles.identityRow}>
<label className={styles.field}>
<span>Type</span>
<select
value={type}
disabled={saving}
onChange={(event) =>
setType(event.target.value as "blog" | "article" | "news")
}
>
{BLOG_TYPES.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</label>
<CatalogCategorySelect
categories={categories}
value={categoryId}
disabled={saving}
optional
placeholder="No category"
onChange={setCategoryId}
/>
</div>
</div>
</section>
<section className={styles.card}>
<h2 className={styles.sectionTitle}>Identity</h2>
<label className={styles.field}>
<span>Title</span>
<input
value={postTitle}
disabled={saving}
required
onChange={(event) => setPostTitle(event.target.value)}
/>
</label>
</section>
<section className={styles.card}>
<h2 className={styles.sectionTitle}>Content</h2>
<div className={styles.stack}>
<label className={styles.field}>
<span>
Abstract <em className={styles.optional}>(optional)</em>
</span>
<textarea
rows={3}
value={abstract}
disabled={saving}
onChange={(event) => setAbstract(event.target.value)}
/>
</label>
<div className={styles.richField}>
<RichTextArea
label="Content"
value={mainTextHtml}
onChange={setMainTextHtml}
disabled={saving}
minHeight={260}
/>
</div>
<TagsField tags={tags} disabled={saving} onChange={setTags} />
</div>
</section>
<section className={styles.card}>
<h2 className={styles.sectionTitle}>SEO</h2>
<div className={styles.identityRow}>
<label className={styles.field}>
<span>
SEO title <em className={styles.optional}>(optional)</em>
</span>
<input
value={seoMetaTitle}
maxLength={70}
disabled={saving}
onChange={(event) => setSeoMetaTitle(event.target.value)}
/>
</label>
<label className={styles.field}>
<span>
SEO description <em className={styles.optional}>(optional)</em>
</span>
<input
value={seoMetaDescription}
maxLength={160}
disabled={saving}
onChange={(event) => setSeoMetaDescription(event.target.value)}
/>
</label>
</div>
</section>
<div className={styles.footerActions}>
<Link href={listHref} className={`outline-btn ${styles.ghostBtn}`}>
Cancel
</Link>
<button
type="submit"
className={`outline-btn ${styles.saveBtn}`}
disabled={saving}
>
{saving ? "Saving…" : "Save"}
</button>
</div>
</div>
</form>
);
}