Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user