274 lines
8.1 KiB
TypeScript
274 lines
8.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
FeaturedImageField,
|
|
GalleryImagesField,
|
|
TagsField,
|
|
type LocalImage,
|
|
} from "@/components/catalog/CatalogFormFields";
|
|
import CatalogCategorySelect from "@/components/catalog/CatalogCategorySelect";
|
|
import RichTextArea from "@/components/RichTextArea";
|
|
import {
|
|
createMeshkeeProduct,
|
|
listMeshkeeEntityCategories,
|
|
uploadMeshkeeMediaFile,
|
|
type MeshkeeEntityCategory,
|
|
} from "@/services/meshkee-catalog";
|
|
import styles from "./CatalogForm.module.css";
|
|
|
|
export default function AddProductForm({
|
|
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 [nameFa, setNameFa] = useState("");
|
|
const [nameEn, setNameEn] = useState("");
|
|
const [summary, setSummary] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [thumbnail, setThumbnail] = useState<LocalImage | null>(null);
|
|
const [gallery, setGallery] = useState<LocalImage[]>([]);
|
|
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, "product");
|
|
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 (!nameEn.trim() && !nameFa.trim()) {
|
|
setError("Enter a product name.");
|
|
return;
|
|
}
|
|
if (!categoryId) {
|
|
setError("Select a category.");
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
setError("");
|
|
try {
|
|
let featuredMediaId: string | undefined;
|
|
if (thumbnail?.file) {
|
|
featuredMediaId = (
|
|
await uploadMeshkeeMediaFile(meshkeeId, thumbnail.file)
|
|
).id;
|
|
} else if (thumbnail?.mediaId) {
|
|
featuredMediaId = thumbnail.mediaId;
|
|
}
|
|
|
|
const galleryMediaIds: string[] = [];
|
|
for (const item of gallery) {
|
|
if (item.mediaId) {
|
|
galleryMediaIds.push(item.mediaId);
|
|
continue;
|
|
}
|
|
if (item.file) {
|
|
galleryMediaIds.push(
|
|
(await uploadMeshkeeMediaFile(meshkeeId, item.file)).id,
|
|
);
|
|
}
|
|
}
|
|
|
|
await createMeshkeeProduct(meshkeeId, {
|
|
title: nameEn.trim() || nameFa.trim(),
|
|
nameFa: nameFa.trim() || undefined,
|
|
summary: summary.trim() || undefined,
|
|
descriptionHtml: description || undefined,
|
|
categoryId,
|
|
featuredMediaId,
|
|
galleryMediaIds,
|
|
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 product.");
|
|
} 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="Thumbnail"
|
|
value={thumbnail}
|
|
disabled={saving}
|
|
onChange={setThumbnail}
|
|
/>
|
|
<div className={styles.stack}>
|
|
<GalleryImagesField
|
|
items={gallery}
|
|
disabled={saving}
|
|
onChange={setGallery}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>Identity</h2>
|
|
<div className={styles.identityRow3}>
|
|
<CatalogCategorySelect
|
|
categories={categories}
|
|
value={categoryId}
|
|
disabled={saving}
|
|
onChange={setCategoryId}
|
|
/>
|
|
<label className={styles.field}>
|
|
<span>Name EN</span>
|
|
<input
|
|
value={nameEn}
|
|
disabled={saving}
|
|
required
|
|
onChange={(event) => setNameEn(event.target.value)}
|
|
/>
|
|
</label>
|
|
<label className={styles.field}>
|
|
<span>Name FA</span>
|
|
<input
|
|
className="fa"
|
|
lang="fa"
|
|
dir="rtl"
|
|
value={nameFa}
|
|
disabled={saving}
|
|
onChange={(event) => setNameFa(event.target.value)}
|
|
/>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<section className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>Content</h2>
|
|
<div className={styles.stack}>
|
|
<label className={styles.field}>
|
|
<span>
|
|
Summary <em className={styles.optional}>(optional)</em>
|
|
</span>
|
|
<textarea
|
|
rows={3}
|
|
value={summary}
|
|
disabled={saving}
|
|
onChange={(event) => setSummary(event.target.value)}
|
|
/>
|
|
</label>
|
|
<div className={styles.richField}>
|
|
<RichTextArea
|
|
label="Description"
|
|
value={description}
|
|
onChange={setDescription}
|
|
disabled={saving}
|
|
minHeight={200}
|
|
/>
|
|
</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>
|
|
);
|
|
}
|