atlp-rwanda/atlp-devpulse-fn

View on GitHub
src/pages/Blogs/allBlogs.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { getAllBlogs, getBlogsByAuthor } from "../../redux/actions/blogActions";
import { createBlogAction } from "../../redux/actions/blogActions";
import * as icons from "react-icons/ai";
import blogSchema from "../../validation/blogSchema"
import { Spinner } from "flowbite-react";
import { handleBlogImageUpload } from "../../utils/imageUploadUtil";
import { useAppDispatch, useAppSelector } from "../../hooks/hooks";
import AllBlogsSkeleton from "../../skeletons/allBlogsSkeleton";
import SingleBlogSkeleton from "skeletons/singleBlogSkeleton";

 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;
  }

 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)[];
}


const AllBlogs = () => {
  const dispatch = useAppDispatch();
  const navigate = useNavigate();
  const [addNewBlogModal, setAddNewBlogModal] = useState(false);
  const [addingBlog, setAddingBlog] = useState(false);
  const [manyImaages, setManyImages] = useState(false);
  const [tags, setTags] = useState('');
  const [submitData, setSubmitData] = useState<SubmitData>({
    title: "",
    content: "",
    tags: [],
    coverImage:"",
    images: [],
  });
  const [errors, setErrors] = useState({
    title: "",
    content: "",
    tags: [""],
    coverImage:"",
    images: [""],
  });
  const [isUploading, setIsUploading] = useState(false);
   
  const userId = localStorage.getItem('userId');
  const role = localStorage.getItem('roleName');
  
  const { data ,isLoading} = useAppSelector((state) => ({data:state.blogs.data,isLoading:state.blogs.isLoading}));
  const blogs = data;
   useEffect(() => {
    if(role=='applicant'){
    dispatch(getBlogsByAuthor(String(userId)));
     }
     else{
       dispatch(getAllBlogs());
     }
  }, [dispatch]);


  const handleBlogClick = (blogId: string) => {
    navigate(`${blogId}`);
  };
  
  
 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  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);
  };
  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="";
    let imageUrls: string[] = [];

     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,
      author: userId,
      tags: submitData.tags || []
    };
    setAddingBlog(true);
    await dispatch(createBlogAction(obj));
    setAddingBlog(false);
    removeModal();
    dispatch(getBlogsByAuthor(String(userId)));
  } catch (error) {
    console.log(error);
  } finally {
    setIsUploading(false);
  }
 };
  
  
  return (
    <div className="min-h-screen bg-white w-full dark:bg-slate-900 dark:text-white p-6">
       {addNewBlogModal && (
        <div className="fixed inset-0 mt-16 p-0 flex items-center justify-center bg-black bg-opacity-20 dark:bg-opacity-40">
          <div className="bg-white dark:bg-dark-bg w-11/12 md:w-3/5 lg:w-2/5 rounded-lg p-6">
            <div className="w-full flex mb-2 items-center justify-between"> 
            <h3 className="font-bold text-m dark:text-white ">
              CREATE A NEW BLOG
              </h3>
               <icons.AiOutlineClose
                className="float-right text-3xl cursor-pointer"
                onClick={() => removeModal()}
              />
            </div>
            <form onSubmit={handleSubmit} className="space-y-2">
              <div className="flex flex-col">
                <label className="font-semibold text-sm">Blog Title</label>
                <input
                  type="text"
                  name="title"
                  value={submitData.title}
                  maxLength={200}
                  onChange={handleInputChange}
                  className="border focus:ring-2 focus:ring-white dark:bg-black rounded px-4 py-2"
                  placeholder="Enter Blog Title"
                />
                {errors.title && <span className="text-red-500 text-xs">{errors.title}</span>}
              </div>
              <div className="flex flex-col">
                <label className="font-semibold text-sm">Blog Content</label>
                <textarea
                  name="content"
                  value={submitData.content}
                  onChange={handleInputChange}
                  className="border rounded focus:ring-2 focus:ring-white dark:bg-black px-4 py-2 h-24"
                  placeholder="Enter Blog Content"
                  maxLength={2000}
                />
                {errors.content && <span className="text-red-500 text-xs">{errors.content}</span>}
              </div>
              <div className="flex flex-col">
                <label className="font-semibold text-sm">Tags (comma separated)</label>
                <input
                  type="text"
                  name="tags"
                  value={tags}
                  maxLength={100}
                  onChange={handleInputChange}
                  className="border rounded focus:ring-2 focus:ring-white dark:bg-black px-4 py-2"
                  placeholder="Tags"
                />
                {errors.tags && <span className="text-red-500 text-xs">{errors.tags}</span>}
              </div>
              <div className="flex flex-col">
                <label className="font-semibold text-sm">Cover Image</label>
                <input
                  type="file"
                  name="coverImage"
                  accept="image/*"
                  onChange={handleInputChange}
                  className="border rounded focus:ring-2 focus:ring-white dark:bg-black px-4 py-2"
                />
                {errors.coverImage && <span className="text-red-500 text-xs">{errors.coverImage}</span>}
              </div>
              <div className="flex flex-col">
                <label className="font-semibold text-sm">Blog Images(Maximum 4)</label>
                <input
                  type="file"
                  name="images"
                  multiple
                  maxLength={4}
                  accept="image/*"
                  onChange={handleInputChange}
                  className="border rounded dark:bg-black px-4 py-2"
                  placeholder="Upload images"
                />
                {manyImaages && <span className="text-red-500 text-xs">Images should not exceed 4</span>}
                {errors.images && <span className="text-red-500 text-xs">{errors.images}</span>}
              </div>

             <button
                      type="submit"
                      disabled={isUploading||addingBlog}
                      className="w-1/3 rounded w-15 px-5 py-1 mt-10 bg-green text-white transition-colors hover:bg-dark-frame-bg hover:text-green hover:border hover:border-green"
                    >
                     {isUploading || addingBlog && <Spinner/> } {isUploading ||addingBlog ? "Submitting..." : "Submit"}
                  </button>
            </form>
          </div>
        </div>
      )}

       <div className="flex w-2/5 bg-gray-100 dark:bg-dark-tertiary rounded-xl h-10 items-center border-2">
        <input
          type="text"
          placeholder="Search Blogs"
          className="bg-transparent outline-none px-2 text-gray-700 dark:text-gray-300 w-96"
        />
        <button className="px-4">
          <icons.AiOutlineSearch className="cursor-pointer text-cyan-300" />
        </button>
      </div>
      <div className="max-w-6xl mt-2 mx-auto">
        <div className="mb-6 w-full flex items-center justify-between">
          <h1 className="text-2xl font-semibold">All Blogs</h1>
          {userId && role && role == 'applicant' &&
            <div className="w-full sm:w-auto">
              <button disabled={isLoading} onClick={Open} className={`flex items-center justify-center w-full sm:w-auto ${isLoading ? 'bg-emerald-300': 'bg-primary dark:bg-[#56C870]'}  rounded-md py-2 px-4 text-white font-medium cursor-pointer hover:opacity-90 transition-opacity`} >
               <icons.AiOutlinePlus className="mr-2" /> Blog
                </button>
              </div>
          }
        </div>

        <div className="space-y-4">
          {isLoading ? (
           <AllBlogsSkeleton/>
          )
              : blogs?.length ? (
              blogs.map((blog: any) => (
              <div
                key={blog.id}
                onClick={() => handleBlogClick(blog.id)}
                className="flex items-center gap-4  bg-slate-100 dark:bg-slate-800 px-4 hover:bg-slate-50 py-2 rounded-lg dark:hover:bg-slate-700 transition-colors cursor-pointer group"
              >
                <div className="h-fit w-1/6 rounded-lg">
                  <img
                    src={blog.coverImage}
                    alt={blog.title}
                    className="w-28 h-28 object-cover rounded-md"
                  />
               </div>
                <div className="flex-grow w-4/6 px-2">
                  <p className="text-lg font-medium break-words whitespace-normal overflow-wrap-break-word transition-colors">
                    {blog.title}
                  </p>
                  <p className="dark:text-slate-400 text-slate-800 break break-words text-sm line-clamp-2">{blog.content}</p>              
                </div>
                <div className="w-1/6 flex flex-col items-end text-sm dar:text-slate-400">
                  <span>{`${blog.author.firstname} ${blog.author.lastname}`}</span>
                  <span>{new Date(Number(blog.created_at)).toLocaleString()}</span>
                </div>
              </div>
            ))
          ) : (
           <div className="flex flex-col items-center justify-center py-12 dark:text-gray-500">
  <div className="dark:bg-gray-100 rounded-full p-6 mb-4 shadow-lg">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      className="h-12 w-12 text-gray-400"
      fill="none"
      viewBox="0 0 24 24"
      stroke="currentColor"
      strokeWidth="2"
    >
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M12 8v4l3 3m9-5a9 9 0 11-18 0 9 9 0 0118 0z"
      />
    </svg>
  </div>
  <h2 className="text-2xl font-semibold dark:text-gray-700 mb-2">No Blogs Available</h2>
  <p className="dark:text-gray-500 text-sm mb-6">
    It looks like there are no blog posts yet. Check back soon for updates!
  </p>
</div>
          )} 
        </div>
      </div>
    </div>
  );
};

export default AllBlogs;