-
{errors.content && (
{errors.content}
@@ -361,19 +408,18 @@ const AllBlogs = () => {
All Blogs
- {userId && role && (role == "applicant" || role == "trainee") && (
-
-
-
- )}
+
+
+
+
@@ -397,9 +443,10 @@ const AllBlogs = () => {
{blog.title}
-
- {blog.content}
-
+
{`${blog.author.firstname} ${blog.author.lastname}`}
@@ -408,10 +455,7 @@ const AllBlogs = () => {
{role === "admin" || role === "superAdmin" ? (
-
+
) : null}
))
diff --git a/src/pages/Blogs/singleBlog.tsx b/src/pages/Blogs/singleBlog.tsx
index 2f52413f..1bc3d7e7 100644
--- a/src/pages/Blogs/singleBlog.tsx
+++ b/src/pages/Blogs/singleBlog.tsx
@@ -1,86 +1,350 @@
-
-import React, { useEffect, useState } from 'react';
-import { useParams } from 'react-router-dom';
-import { Heart, MessageCircle,User } from 'lucide-react';
-import { useAppDispatch,useAppSelector } from '../../hooks/hooks';
-import { getBlogById, getBlogRelatedArticles, deleteBlogAction } from "../../redux/actions/blogActions";
-import { Spinner } from 'flowbite-react';
-import SingleBlogSkeleton from '../../skeletons/singleBlogSkeleton';
+import React, { useEffect, useState, ChangeEvent } from "react";
+import { useParams } from "react-router-dom";
+import { Heart, MessageCircle, User } from "lucide-react";
+import { useAppDispatch, useAppSelector } from "../../hooks/hooks";
+import {
+ getBlogById,
+ getBlogRelatedArticles,
+ deleteBlogAction,
+ updateBlogAction,
+} from "../../redux/actions/blogActions";
+import blogSchema from "../../validation/blogSchema";
+import { handleBlogImageUpload } from "../../utils/imageUploadUtil";
+import { Spinner } from "flowbite-react";
+import SingleBlogSkeleton from "../../skeletons/singleBlogSkeleton";
import * as icons from "react-icons/ai";
-import { useSelector } from 'react-redux';
-import { toast } from 'react-toastify';
-import BlogComment from './BlogComment';
-import BlogReaction from './BlogReactions';
+import { useSelector } from "react-redux";
+import { toast } from "react-toastify";
+import { useNavigate } from "react-router-dom";
+import ReactQuill from "react-quill";
+import "react-quill/dist/quill.snow.css";
+import { AiOutlineClose } from "react-icons/ai";
+
+interface Comment {
+ _id: String;
+ content: String;
+ blog: Blog;
+ createdAt: String;
+}
+interface Like {
+ _id: String;
+ blog: Blog;
+ created_at: String;
+}
+
+interface User {
+ _id: String;
+ createdAt: String;
+ firstname: String;
+ lastname: String;
+ email: String;
+ role: String;
+ profile: String;
+ isEmailVerified: Boolean;
+ status: Boolean;
+ resetToken: String;
+}
-import { useNavigate } from 'react-router-dom';
+interface Blog {
+ _id: String;
+ title: String;
+ content: String;
+ coverImage: String;
+ images: [String];
+ author: User;
+ tags: [String];
+ isHidden: Boolean;
+ created_at: String;
+ updated_at: String;
+ likes: [Like];
+ comments: [Comment];
+}
+
+interface SubmitData {
+ title: string;
+ content: string;
+ tags: string[];
+ coverImage: File | string;
+ images: (File | string)[];
+}
+import BlogComment from "./BlogComment";
+import BlogReaction from "./BlogReactions";
const SingleBlogView = () => {
const { id } = useParams();
- const [comment, setComment] = useState('');
+ const [comment, setComment] = useState("");
const [likes, setLikes] = useState(20);
const [isLiked, setIsLiked] = useState(false);
- const userId = localStorage.getItem('userId');
+ const userId = localStorage.getItem("userId");
const dispatch = useAppDispatch();
const navigate = useNavigate();
- const [deleteBlogModal,setDeleteBlogModal] = useState(false)
-
+ const [deleteBlogModal, setDeleteBlogModal] = useState(false);
+
const handleLike = () => {
setIsLiked(!isLiked);
setLikes(isLiked ? likes - 1 : likes + 1);
};
- const {
- blogRelatedArticles,isLoading: isLoadingRelatedArticles } = useAppSelector((state) =>
- ({blogRelatedArticles: state.blogRelatedArticle.blogRelatedArticles,
- isLoading: state.blogRelatedArticle.isLoading,}));
- const topArticles = Array.isArray(blogRelatedArticles)? blogRelatedArticles.sort(() => Math.random() - 0.5).slice(0, 3): [];
+ const { blogRelatedArticles, isLoading: isLoadingRelatedArticles } =
+ useAppSelector((state) => ({
+ blogRelatedArticles: state.blogRelatedArticle.blogRelatedArticles,
+ isLoading: state.blogRelatedArticle.isLoading,
+ }));
+ const topArticles = Array.isArray(blogRelatedArticles)
+ ? blogRelatedArticles.sort(() => Math.random() - 0.5).slice(0, 3)
+ : [];
useEffect(() => {
- const blogId: any = id;
- dispatch(getBlogRelatedArticles(blogId))
- }, [dispatch, id]);
-
+ const blogId: any = id;
+ dispatch(getBlogRelatedArticles(blogId));
+ }, [dispatch, id]);
+
const handleComment = (e) => {
e.preventDefault();
// Add comment logic here
- setComment('');
+ setComment("");
};
- const { data ,isLoading} = useAppSelector((state) => ({data:state.singleBlog.data,isLoading:state.singleBlog.isLoading}));
+ const { data, isLoading } = useAppSelector((state) => ({
+ data: state.singleBlog.data,
+ isLoading: state.singleBlog.isLoading,
+ }));
const blog = data;
useEffect(() => {
- if(id)
- dispatch(getBlogById(id));
+ if (id) dispatch(getBlogById(id));
}, [dispatch]);
- const handleDeleteBlog = async (id: string) => {
- try {
- const result = await dispatch(deleteBlogAction(id));
+ const handleDeleteBlog = async (id: string) => {
+ try {
+ const result = await dispatch(deleteBlogAction(id));
- // Check the result's type for success or failure
- if (result.type === "DELETE_BLOG_SUCCESS") {
- toast.success("Blog deleted");
- navigate(-1); // Go back to the previous page
- } else {
- toast.error("Failed to delete blog! Try again");
+ // Check the result's type for success or failure
+ if (result.type === "DELETE_BLOG_SUCCESS") {
+ toast.success("Blog deleted");
+ navigate(-1); // Go back to the previous page
+ } else {
+ toast.error("Failed to delete blog! Try again");
+ }
+ } catch (error: any) {
+ // This block is now for unexpected errors
+ toast.error(error.message || "Unexpected error! Try again");
}
- } catch (error: any) {
- // This block is now for unexpected errors
- toast.error(error.message || "Unexpected error! Try again");
- }
-};
-
+ };
const openDeleteModal = () => {
- setDeleteBlogModal(true)
- }
+ setDeleteBlogModal(true);
+ };
const closeDeleteModal = () => {
- setDeleteBlogModal(false)
- }
+ setDeleteBlogModal(false);
+ };
+
+ const [addNewBlogModal, setAddNewBlogModal] = useState(false);
+ const [addingBlog, setAddingBlog] = useState(false);
+ const [manyImaages, setManyImages] = useState(false);
+ const [tags, setTags] = useState("");
+ const [submitData, setSubmitData] = useState
({
+ title: "",
+ content: "",
+ tags: [],
+ coverImage: "",
+ images: [],
+ });
+ const [errors, setErrors] = useState({
+ title: "",
+ content: "",
+ tags: [""],
+ coverImage: "",
+ images: [""],
+ });
+ const [isUploading, setIsUploading] = useState(false);
+
+ const handleInputChange = (
+ e: React.ChangeEvent
+ ) => {
+ const { name, value } = e.target;
+
+ if (e.target instanceof HTMLInputElement && e.target.files) {
+ const files = e.target.files;
+
+ if (name === "coverImage" && files.length > 0) {
+ setSubmitData((prevState) => ({
+ ...prevState,
+ coverImage: files[0],
+ }));
+ } else if (name === "images") {
+ if (files.length > 4) {
+ setManyImages(true);
+ return;
+ }
+ setSubmitData((prevState) => ({
+ ...prevState,
+ images: Array.from(files),
+ }));
+ setManyImages(false);
+ }
+ } else if (name === "tags") {
+ const tagArray = value
+ .split(",")
+ .map((tag) => tag.trim())
+ .filter((tag) => tag !== "");
+ setSubmitData((prevState) => ({ ...prevState, tags: tagArray }));
+ setTags(value);
+ } else {
+ setSubmitData((prevState) => ({ ...prevState, [name]: value }));
+ }
+ };
+
+ const Open = () => {
+ setAddNewBlogModal(true);
+ setSubmitData({
+ title: blog.title,
+ content: blog.content,
+ tags: blog.tags,
+ coverImage: "",
+ images: [],
+ });
+ setTags(blog.tags.join(", "));
+ setContent(blog.content);
+ };
+ const removeModal = () => {
+ let newState = !addNewBlogModal;
+ setAddNewBlogModal(newState);
+
+ setSubmitData({
+ title: "",
+ content: "",
+ tags: [""],
+ coverImage: "",
+ images: [""],
+ });
+
+ setErrors({
+ title: "",
+ content: "",
+ tags: [""],
+ coverImage: "",
+ images: [""],
+ });
+ };
+ const validateForm = (data: any, schema: any) => {
+ const { error } = schema.validate(data, { abortEarly: false });
+ if (!error) {
+ //@ts-ignore
+ setErrors({});
+ return true;
+ }
+
+ const newErrors = {};
+ error.details.forEach((detail: any) => {
+ newErrors[detail.path[0]] = detail.message;
+ });
+
+ //@ts-ignore
+ setErrors(newErrors);
+ return false;
+ };
+
+ const handleSubmit = async (e: any) => {
+ e.preventDefault();
+ setIsUploading(true);
+
+ const isValid = validateForm(submitData, blogSchema);
+ if (!isValid) {
+ setIsUploading(false);
+ return;
+ }
+
+ try {
+ let coverImageUrl: string | null = blog.coverImage;
+ let imageUrls: string[] = blog.images;
+
+ if (submitData.coverImage instanceof File) {
+ coverImageUrl = await handleBlogImageUpload(
+ submitData.coverImage,
+ (url) => {
+ coverImageUrl = url;
+ },
+ setIsUploading
+ );
+ }
+
+ if (submitData.images.length > 0) {
+ const results = await Promise.all(
+ submitData.images.map(async (file) => {
+ if (file instanceof File) {
+ return handleBlogImageUpload(file, (url) => url, setIsUploading);
+ }
+ return file;
+ })
+ );
+ imageUrls = results.filter((url): url is string => url !== null);
+ }
+
+ const obj = {
+ title: submitData.title,
+ content: submitData.content,
+ coverImage: coverImageUrl,
+ images: imageUrls,
+ tags: submitData.tags || [],
+ };
+ setAddingBlog(true);
+ await dispatch(updateBlogAction(blog.id, obj));
+ setAddingBlog(false);
+ removeModal();
+ dispatch(getBlogById(id as string));
+ } catch (error) {
+ console.log(error);
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ const [content, setContent] = useState(submitData.content || "");
+
+ const modules = {
+ toolbar: [
+ [{ header: [1, 2, 3, 4, 5, 6, false] }],
+ ["bold", "italic", "underline", "strike"],
+ [{ list: "ordered" }, { list: "bullet" }],
+ ["link"],
+ [{ align: [] }],
+ [{ color: [] }, { background: [] }],
+ ["clean"],
+ ],
+ };
+
+ const formats = [
+ "header",
+ "bold",
+ "italic",
+ "underline",
+ "strike",
+ "list",
+ "bullet",
+ "link",
+ "image",
+ "align",
+ "color",
+ "background",
+ ];
+
+ const handleContentChange = (value: string) => {
+ setContent(value);
+
+ // Simulate an event object to match the expected input of handleInputChange
+ const syntheticEvent = {
+ target: {
+ name: "content",
+ value: value,
+ },
+ } as ChangeEvent;
+
+ handleInputChange(syntheticEvent);
+ };
return (
- {deleteBlogModal && (
+ {deleteBlogModal && (
@@ -94,16 +358,131 @@ const SingleBlogView = () => {
className="float-right text-2xl cursor-pointer"
onClick={() => closeDeleteModal()}
/>
-
-
)}
- {isLoading ||isLoadingRelatedArticles || !blog ? (
+ {addNewBlogModal && (
+
+ )}
+ {isLoading || isLoadingRelatedArticles || !blog ? (
) : (
@@ -133,12 +512,28 @@ const SingleBlogView = () => {
+
+
+ Edit
+
+