blog
This commit is contained in:
parent
7825858e00
commit
9d77f5293c
@ -1,101 +1,151 @@
|
|||||||
import { Metadata } from 'next';
|
"use client";
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
import { buildApiUrl } from '@/utils/BaseUrl.utils';
|
||||||
|
import { showMessage } from '@/utils/CommonFunction.utils';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
interface BlogItem {
|
||||||
title: 'Blog',
|
id: number;
|
||||||
};
|
title: string;
|
||||||
|
slug: string;
|
||||||
const blogs = [
|
image: string;
|
||||||
{
|
description: string;
|
||||||
id: 1,
|
author: string;
|
||||||
image: '/assets/images/blog/image-1.jpg',
|
profile: string;
|
||||||
title: 'Excessive sugar is harmful',
|
date: string;
|
||||||
description: 'Sugar consumption can have serious effects on your health if taken in excess. Learn how to reduce it.',
|
}
|
||||||
author: 'Alma Clark',
|
|
||||||
profile: '/assets/images/profile-1.jpeg',
|
|
||||||
date: '06 May',
|
|
||||||
slug: '/blog/1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
image: '/assets/images/blog/image-1.jpg',
|
|
||||||
title: 'Creative Photography',
|
|
||||||
description: 'Photography is not just about capturing pictures, but emotions and stories through your lens.',
|
|
||||||
author: 'Alma Clark',
|
|
||||||
profile: '/assets/images/profile-2.jpeg',
|
|
||||||
date: '06 May',
|
|
||||||
slug: '/blog/2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
image: '/assets/images/blog/image-1.jpg',
|
|
||||||
title: 'Plan your next trip',
|
|
||||||
description: 'Traveling helps you explore new cultures, food, and make memories that last a lifetime.',
|
|
||||||
author: 'Alma Clark',
|
|
||||||
profile: '/assets/images/profile-3.jpeg',
|
|
||||||
date: '06 May',
|
|
||||||
slug: '/blog/3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
image: '/assets/images/blog/image-1.jpg',
|
|
||||||
title: 'My latest Vlog',
|
|
||||||
description: 'Check out my latest vlog where I share behind-the-scenes of my daily life and adventures.',
|
|
||||||
author: 'Alma Clark',
|
|
||||||
profile: '/assets/images/profile-4.jpeg',
|
|
||||||
date: '06 May',
|
|
||||||
slug: '/blog/4',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Blog = () => {
|
const Blog = () => {
|
||||||
|
const [blogs, setBlogs] = useState<BlogItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchBlogs = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(buildApiUrl('blogs'));
|
||||||
|
if (res.data && res.data.success) {
|
||||||
|
setBlogs(res.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching blogs:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBlogs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, delete it!'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const token = cookies.get('token');
|
||||||
|
if (!token) {
|
||||||
|
showMessage('Access denied. Please sign in first.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.delete(buildApiUrl(`blogs/${id}`), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setBlogs(prev => prev.filter(b => b.id !== id));
|
||||||
|
showMessage("Blog Deleted Successfully", "success");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error deleting blog:", error);
|
||||||
|
showMessage(error?.response?.data?.message || "Error deleting blog", "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripHtml = (htmlStr: string) => {
|
||||||
|
if (!htmlStr) return '';
|
||||||
|
return htmlStr.replace(/<[^>]*>/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h3 className="mb-6 text-xl font-bold md:text-3xl">Blogs</h3>
|
<h3 className="mb-6 text-xl font-bold md:text-3xl">Blogs</h3>
|
||||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
{/* ✅ First box: Create New Blog */}
|
{/* ✅ First box: Create New Blog */}
|
||||||
<Link href="/create-blog" className="flex items-center justify-center space-y-5 rounded-md border border-dashed border-blue-500 bg-blue-50 p-5 text-center shadow hover:bg-blue-100 transition dark:border-blue-800 dark:bg-blue-900 dark:hover:bg-blue-800">
|
<Link href="/create-blog" className="flex items-center justify-center space-y-5 rounded-md border border-dashed border-blue-500 bg-blue-50 p-5 text-center shadow hover:bg-blue-100 transition dark:border-blue-800 dark:bg-blue-900 dark:hover:bg-blue-800 min-h-[300px]">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-3 text-5xl text-blue-600 dark:text-white">+</div>
|
<div className="mb-3 text-5xl text-blue-600 dark:text-white">+</div>
|
||||||
<h5 className="text-lg font-semibold text-blue-800 dark:text-white">Create New Blog</h5>
|
<h5 className="text-lg font-semibold text-blue-800 dark:text-white">Create New Blog</h5>
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
{blogs.map((blog) => (
|
|
||||||
<div
|
|
||||||
key={blog.id}
|
|
||||||
className="space-y-4 rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black"
|
|
||||||
>
|
|
||||||
<div className="max-h-56 overflow-hidden rounded-md">
|
|
||||||
<img src={blog.image} alt={blog.title} className="w-full object-cover" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ✅ Description first, then Title */}
|
|
||||||
<h5 className="text-lg font-semibold dark:text-white">{blog.title}</h5>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{blog.description}</p>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="me-4 overflow-hidden rounded-full bg-white-dark">
|
|
||||||
<img src={blog.profile} className="h-11 w-11 object-cover" alt={blog.author} />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="mb-1.5 font-semibold dark:text-white">{blog.author}</h4>
|
|
||||||
<p className="text-xs text-gray-500">{blog.date}</p>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* ✅ Read More button */}
|
|
||||||
<div>
|
|
||||||
<Link
|
|
||||||
href={blog.slug}
|
|
||||||
className="inline-block mt-3 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Read More
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</Link>
|
||||||
|
{blogs.map((blog) => {
|
||||||
|
const cleanText = stripHtml(blog.description);
|
||||||
|
const excerpt = cleanText.length > 120 ? cleanText.substring(0, 120) + '...' : cleanText;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={blog.id}
|
||||||
|
className="flex flex-col justify-between space-y-4 rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="max-h-56 overflow-hidden rounded-md mb-4 bg-gray-100 flex items-center justify-center aspect-video">
|
||||||
|
<img src={blog.image || '/assets/images/blog/image-1.jpg'} alt={blog.title} className="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 className="text-lg font-semibold dark:text-white line-clamp-2" title={blog.title}>{blog.title}</h5>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2 line-clamp-3">{excerpt || 'No description provided.'}</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-3">By {blog.author} on {blog.date}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ✅ Actions (View, Edit and Delete) */}
|
||||||
|
<div className="flex justify-between items-center pt-3 border-t border-gray-100 dark:border-gray-800 space-x-2">
|
||||||
|
<a
|
||||||
|
href={`http://localhost:3000/blog-single?slug=${blog.slug}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-block rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
View Blog
|
||||||
|
</a>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Link
|
||||||
|
href={`/edit-blog/${blog.id}`}
|
||||||
|
className="inline-block rounded-lg bg-green-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-green-700 transition"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(blog.id)}
|
||||||
|
className="rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 transition"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,11 @@ import IconTrashLines from "@/components/icon/icon-trash-lines";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import "react-quill/dist/quill.snow.css";
|
import "react-quill/dist/quill.snow.css";
|
||||||
|
import axios from "axios";
|
||||||
|
import Cookies from "universal-cookie";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { showMessage } from "@/utils/CommonFunction.utils";
|
||||||
|
import { buildApiUrl } from "@/utils/BaseUrl.utils";
|
||||||
|
|
||||||
const ReactQuill = dynamic(() => import("react-quill"), {
|
const ReactQuill = dynamic(() => import("react-quill"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -26,11 +31,14 @@ const formats = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const PostForm = () => {
|
const PostForm = () => {
|
||||||
|
const router = useRouter();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: "",
|
title: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
coverImage: null as File | null,
|
coverImage: null as File | null,
|
||||||
description: "",
|
description: "",
|
||||||
|
date: "",
|
||||||
|
author: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const modules = useMemo(
|
const modules = useMemo(
|
||||||
@ -111,25 +119,68 @@ const PostForm = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const data = new FormData();
|
if (!formData.title.trim()) {
|
||||||
data.append("title", formData.title);
|
showMessage("Title is required", "error");
|
||||||
data.append("slug", formData.slug);
|
return;
|
||||||
|
|
||||||
if (formData.coverImage) {
|
|
||||||
data.append("coverImage", formData.coverImage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.append("description", formData.description);
|
try {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const token = cookies.get('token');
|
||||||
|
if (!token) {
|
||||||
|
showMessage('Access denied. Please sign in first.', 'error');
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("FormData prepared:", {
|
let coverImageUrl = "";
|
||||||
title: formData.title,
|
|
||||||
slug: formData.slug,
|
if (formData.coverImage) {
|
||||||
coverImage: formData.coverImage?.name,
|
const data = new FormData();
|
||||||
description: formData.description,
|
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 createData = {
|
||||||
|
title: formData.title,
|
||||||
|
slug: cleanSlug,
|
||||||
|
image: coverImageUrl,
|
||||||
|
description: formData.description,
|
||||||
|
date: formData.date,
|
||||||
|
author: formData.author,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await axios.post(buildApiUrl('blogs'), createData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Create blog response:", res.data);
|
||||||
|
showMessage("Blog Created Successfully", "success");
|
||||||
|
router.push("/blog");
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error creating blog:", error);
|
||||||
|
showMessage(error?.response?.data?.message || "Error creating blog", "error");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -163,6 +214,30 @@ const PostForm = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="block font-medium mb-1">Cover Image</label>
|
<label className="block font-medium mb-1">Cover Image</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
335
app/(defaults)/(blog)/edit-blog/[id]/page.tsx
Normal file
335
app/(defaults)/(blog)/edit-blog/[id]/page.tsx
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
"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;
|
||||||
Loading…
x
Reference in New Issue
Block a user