336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import IconTrashLines from "@/components/icon/icon-trash-lines";
|
|
import dynamic from "next/dynamic";
|
|
import React, { useMemo, useState, useEffect } from "react";
|
|
import "react-quill/dist/quill.snow.css";
|
|
import axios from "axios";
|
|
import Cookies from "universal-cookie";
|
|
import { useRouter, useParams } from "next/navigation";
|
|
import { showMessage } from "@/utils/CommonFunction.utils";
|
|
import { buildApiUrl } from "@/utils/BaseUrl.utils";
|
|
|
|
const ReactQuill = dynamic(() => import("react-quill"), {
|
|
ssr: false,
|
|
});
|
|
|
|
const formats = [
|
|
"header",
|
|
"bold",
|
|
"italic",
|
|
"underline",
|
|
"strike",
|
|
"blockquote",
|
|
"code-block",
|
|
"list",
|
|
"bullet",
|
|
"align",
|
|
"link",
|
|
"image",
|
|
"video",
|
|
];
|
|
|
|
const EditPostForm = () => {
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const id = params.id;
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [existingImage, setExistingImage] = useState<string>("");
|
|
|
|
const [formData, setFormData] = useState({
|
|
title: "",
|
|
slug: "",
|
|
coverImage: null as File | null,
|
|
description: "",
|
|
date: "",
|
|
author: "",
|
|
});
|
|
|
|
useEffect(() => {
|
|
const fetchBlog = async () => {
|
|
try {
|
|
const res = await axios.get(buildApiUrl(`blogs/${id}`));
|
|
if (res.data && res.data.success) {
|
|
const blog = res.data.data;
|
|
setFormData({
|
|
title: blog.title || "",
|
|
slug: blog.slug || "",
|
|
coverImage: null,
|
|
description: blog.description || "",
|
|
date: blog.date || "",
|
|
author: blog.author || "",
|
|
});
|
|
if (blog.image) {
|
|
setExistingImage(blog.image);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching blog details:", error);
|
|
showMessage("Failed to load blog data", "error");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (id) {
|
|
fetchBlog();
|
|
}
|
|
}, [id]);
|
|
|
|
const modules = useMemo(
|
|
() => ({
|
|
toolbar: {
|
|
container: [
|
|
[{ header: [1, 2, 3, false] }],
|
|
["bold", "italic", "underline", "strike"],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
["blockquote", "code-block"],
|
|
[{ align: [] }],
|
|
["link", "image", "video"],
|
|
["clean"],
|
|
],
|
|
handlers: {
|
|
image: function (this: any) {
|
|
const input = document.createElement("input");
|
|
input.setAttribute("type", "file");
|
|
input.setAttribute("accept", "image/*");
|
|
input.click();
|
|
|
|
input.onchange = async () => {
|
|
const file = input.files?.[0];
|
|
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = () => {
|
|
const quill = this.quill;
|
|
const range = quill.getSelection(true);
|
|
|
|
quill.insertEmbed(
|
|
range.index,
|
|
"image",
|
|
reader.result
|
|
);
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { name, value } = e.target;
|
|
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[name]: value,
|
|
}));
|
|
};
|
|
|
|
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0] || null;
|
|
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
coverImage: file,
|
|
}));
|
|
setExistingImage(""); // Clear existing preview if new file selected
|
|
};
|
|
|
|
const handleRemoveImage = () => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
coverImage: null,
|
|
}));
|
|
setExistingImage(""); // User wants to completely remove image
|
|
};
|
|
|
|
const handleDescriptionChange = (value: string) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
description: value,
|
|
}));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!formData.title.trim()) {
|
|
showMessage("Title is required", "error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const cookies = new Cookies();
|
|
const token = cookies.get('token');
|
|
if (!token) {
|
|
showMessage('Access denied. Please sign in first.', 'error');
|
|
router.push('/login');
|
|
return;
|
|
}
|
|
|
|
let coverImageUrl = existingImage; // Use existing image unless new one uploaded or removed
|
|
|
|
if (formData.coverImage) {
|
|
const data = new FormData();
|
|
data.append("file", formData.coverImage);
|
|
|
|
const imageUpload = await axios.post(buildApiUrl('upload/single'), data, {
|
|
headers: {
|
|
"Content-Type": "multipart/form-data",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (imageUpload.data && imageUpload.data.success) {
|
|
coverImageUrl = imageUpload.data.data.fullUrl;
|
|
}
|
|
}
|
|
|
|
// Generate clean slug from title if not specified
|
|
const cleanSlug = formData.slug.trim()
|
|
? formData.slug.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '')
|
|
: formData.title.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '');
|
|
|
|
const updateData = {
|
|
title: formData.title,
|
|
slug: cleanSlug,
|
|
image: coverImageUrl,
|
|
description: formData.description,
|
|
date: formData.date,
|
|
author: formData.author,
|
|
};
|
|
|
|
const res = await axios.put(buildApiUrl(`blogs/${id}`), updateData, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
console.log("Update blog response:", res.data);
|
|
showMessage("Blog Updated Successfully", "success");
|
|
router.push("/blog");
|
|
} catch (error: any) {
|
|
console.error("Error updating blog:", error);
|
|
showMessage(error?.response?.data?.message || "Error updating blog", "error");
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="text-center p-10">Loading blog details...</div>;
|
|
}
|
|
|
|
return (
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="space-y-5 max-w-4xl mx-auto p-6 bg-white rounded shadow-md mt-10"
|
|
>
|
|
<h2 className="text-xl font-bold mb-4">Edit Blog</h2>
|
|
|
|
<div>
|
|
<label className="block font-medium mb-1">Blog Title</label>
|
|
<input
|
|
type="text"
|
|
name="title"
|
|
value={formData.title}
|
|
onChange={handleChange}
|
|
placeholder="Enter blog title"
|
|
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium mb-1">Slug</label>
|
|
<input
|
|
type="text"
|
|
name="slug"
|
|
value={formData.slug}
|
|
onChange={handleChange}
|
|
placeholder="Enter slug"
|
|
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium mb-1">Date</label>
|
|
<input
|
|
type="text"
|
|
name="date"
|
|
value={formData.date}
|
|
onChange={handleChange}
|
|
placeholder="e.g. 12 Aug 2026"
|
|
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium mb-1">Author</label>
|
|
<input
|
|
type="text"
|
|
name="author"
|
|
value={formData.author}
|
|
onChange={handleChange}
|
|
placeholder="e.g. Gisselle"
|
|
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-purple-500 outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium mb-1">Cover Image</label>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleImageChange}
|
|
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{(formData.coverImage || existingImage) && (
|
|
<div className="mt-3">
|
|
<p className="text-sm font-medium">Preview:</p>
|
|
<div className="relative inline-block mt-2">
|
|
<img
|
|
src={formData.coverImage ? URL.createObjectURL(formData.coverImage) : existingImage}
|
|
alt="Selected"
|
|
className="w-48 h-32 object-cover rounded border"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={handleRemoveImage}
|
|
className="absolute top-1 right-1 bg-red-600 text-white text-xs px-2 py-1 rounded hover:bg-red-700"
|
|
>
|
|
<IconTrashLines className="shrink-0" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mb-5">
|
|
<ReactQuill
|
|
value={formData.description}
|
|
onChange={handleDescriptionChange}
|
|
modules={modules}
|
|
formats={formats}
|
|
placeholder="Write your description..."
|
|
className="bg-white text-black rounded-md"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="bg-blue-600 text-white px-6 py-2 mt-5 rounded-md hover:bg-blue-700 transition"
|
|
>
|
|
Update Blog
|
|
</button>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export default EditPostForm;
|