diff --git a/app/(defaults)/(blog)/blog/page.tsx b/app/(defaults)/(blog)/blog/page.tsx index 3334644..07f8a0d 100644 --- a/app/(defaults)/(blog)/blog/page.tsx +++ b/app/(defaults)/(blog)/blog/page.tsx @@ -1,101 +1,151 @@ -import { Metadata } from 'next'; +"use client"; + 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 = { - title: 'Blog', -}; - -const blogs = [ - { - id: 1, - image: '/assets/images/blog/image-1.jpg', - title: 'Excessive sugar is harmful', - 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', - }, -]; +interface BlogItem { + id: number; + title: string; + slug: string; + image: string; + description: string; + author: string; + profile: string; + date: string; +} const Blog = () => { + const [blogs, setBlogs] = useState([]); + 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 ( +
+
+
+ ); + } + return (

Blogs

- {/* ✅ First box: Create New Blog */} - -
-
+
-
Create New Blog
-
- - {blogs.map((blog) => ( -
-
- {blog.title} -
- - {/* ✅ Description first, then Title */} -
{blog.title}
-

{blog.description}

- - {/* -
-
- {blog.author} -
-
-

{blog.author}

-

{blog.date}

-
-
*/} - - {/* ✅ Read More button */} -
- - Read More - -
+ {/* ✅ First box: Create New Blog */} + +
+
+
+
Create New Blog
- ))} + + {blogs.map((blog) => { + const cleanText = stripHtml(blog.description); + const excerpt = cleanText.length > 120 ? cleanText.substring(0, 120) + '...' : cleanText; + + return ( +
+
+
+ {blog.title} +
+ +
{blog.title}
+

{excerpt || 'No description provided.'}

+

By {blog.author} on {blog.date}

+
+ + {/* ✅ Actions (View, Edit and Delete) */} +
+ + View Blog + +
+ + Edit + + +
+
+
+ ); + })}
); diff --git a/app/(defaults)/(blog)/create-blog/page.tsx b/app/(defaults)/(blog)/create-blog/page.tsx index 2ace063..614456b 100644 --- a/app/(defaults)/(blog)/create-blog/page.tsx +++ b/app/(defaults)/(blog)/create-blog/page.tsx @@ -4,6 +4,11 @@ import IconTrashLines from "@/components/icon/icon-trash-lines"; import dynamic from "next/dynamic"; import React, { useMemo, useState } from "react"; 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"), { ssr: false, @@ -26,11 +31,14 @@ const formats = [ ]; const PostForm = () => { + const router = useRouter(); const [formData, setFormData] = useState({ title: "", slug: "", coverImage: null as File | null, description: "", + date: "", + author: "", }); const modules = useMemo( @@ -111,25 +119,68 @@ const PostForm = () => { })); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const data = new FormData(); - data.append("title", formData.title); - data.append("slug", formData.slug); - - if (formData.coverImage) { - data.append("coverImage", formData.coverImage); + if (!formData.title.trim()) { + showMessage("Title is required", "error"); + return; } - 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:", { - title: formData.title, - slug: formData.slug, - coverImage: formData.coverImage?.name, - description: formData.description, - }); + let coverImageUrl = ""; + + 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 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 ( @@ -163,6 +214,30 @@ const PostForm = () => { />
+
+ + +
+ +
+ + +
+
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(""); + + 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) => { + const { name, value } = e.target; + + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleImageChange = (e: React.ChangeEvent) => { + 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
Loading blog details...
; + } + + return ( +
+

Edit Blog

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {(formData.coverImage || existingImage) && ( +
+

Preview:

+
+ Selected + +
+
+ )} + +
+ +
+ + +
+ ); +}; + +export default EditPostForm;