Files
dashboards/components/catalog/AddProductForm.tsx
T

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>
);
}