Snai3i-MarketPlace / frontend / src / pages / Dashboard / MyCoursesInstDesigner / components / CreateCourseByInst.tsx
CreateCourseByInst.tsx
Raw
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useFormik, Form, FormikProvider } from 'formik';
import { toast } from 'sonner';
import * as Yup from 'yup';
import Fallback from '@/components/Fallback';
import { useNavigate, useParams } from 'react-router-dom';
import {
  useCreateMarketCourseMutation,
  useGetMarketCourseByIdQuery,
  useGetTagsQuery,
  useUpdateMarketCourseMutation,
} from '@/app/backend/endpoints/courses';
import { useEffect, useRef, useState } from 'react';
import { apiUrl } from '@/app/backend';
import { UploadCloudIcon } from 'lucide-react';
import CreatableSelect from 'react-select/creatable';
import useUser from '@/hooks/useUser';
import useTitle from '@/hooks/useTitle';

function CreateCourseByInst({ isEdit = false }: { isEdit?: boolean }) {
  useTitle('Create Course');
  const { courseId } = useParams();
  const { user } = useUser();
  const navigate = useNavigate();
  const { data: courseDataWrap, isLoading: isGetLoading } =
    useGetMarketCourseByIdQuery(parseInt(courseId ?? ''), { skip: !isEdit });
  const courseData = courseDataWrap?.data;
  const [fileInputKey, setFileInputKey] = useState(
    new Date().getTime().toString()
  );
  const [isLoadingImage, setIsLoadingImage] = useState(false);

  const [createCourse, { isLoading }] = useCreateMarketCourseMutation();
  const [updateCourse, { isLoading: isUpdating }] =
    useUpdateMarketCourseMutation();
  const fileInputRef = useRef<HTMLInputElement>(null);
  const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    fileInputRef.current?.click();
  };

  const { data: tagsDataWrap, isLoading: isGetTags } = useGetTagsQuery();
  const tagsData = tagsDataWrap?.data;
  const [tags, setTags] = useState<
    {
      label: string;
      value: string;
    }[]
  >([]);
  useEffect(() => {
    if (tagsData) {
      setTags(
        tagsData?.map((tag) => ({
          label: tag,
          value: tag,
        }))
      );
    }
  }, [tagsData]);

  const formik = useFormik<CEMarketCourseI>({
    initialValues: {
      inst_designer_id: (user as InstructionalDesigner)?.user_id.toString(),
      inst_designer_firstName: (user as InstructionalDesigner)?.firstName,
      inst_designer_lastName: (user as InstructionalDesigner)?.lastName,
      title: courseData?.title ?? '',
      description: courseData?.description ?? '',
      price: courseData?.price ?? 0,
      tags: courseData?.tags ?? [],
      videoThumbnail: courseData?.videoThumbnail ?? '',
      thumbnail: courseData?.thumbnail ?? '',
      totalHours: courseData?.totalHours ?? 0,
      chaptersCount: courseData?.chaptersCount ?? 0,
      type: 'market',
    },
    validationSchema: Yup.object().shape({
      title: Yup.string().required('Title is required'),
      description: Yup.string().required('Description is required'),
      price: Yup.number()
        .min(1, 'Price is required')
        .required('Price is required'),
      totalHours: Yup.number()
        .min(1, 'Total hours is required')
        .required('Total hours is required'),
      chaptersCount: Yup.number()
        .min(1, 'Chapters count is required')
        .required('Chapters count is required'),
      inst_designer_id: Yup.string().required(
        'Instructor designer is required'
      ),
      thumbnail: Yup.string()
        .min(1, 'Thumbnail is required')
        .required('Thumbnail is required'),
    }),
    enableReinitialize: true,
    validateOnMount: false,
    validateOnBlur: false,
    validateOnChange: false,
    onSubmit: (course: CEMarketCourseI) => {
      if (isEdit) {
        updateCourse({ course_id: Number(courseId), ...course })
          .unwrap()
          .then((response) => {
            toast.success(response.message);
            navigate('/dashboard/courses/instructor');
          })
          .catch((error: any) => {
            toast.error(error?.data?.message ?? error?.error.toString());
          });
      } else {
        createCourse(course)
          .unwrap()
          .then((response) => {
            toast.success(response.message);
            navigate('/dashboard/courses/instructor');
          })
          .catch((error: any) => {
            toast.error(error?.data?.message ?? error?.error.toString());
          });
      }
    },
  });
  const {
    getFieldProps,
    handleSubmit,
    touched,
    errors,
    setFieldValue,
    values,
  } = formik;
  const handleFileChange = (e: any) => {
    const file = e.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = () => {
        if (reader.readyState === 2) {
          // upload file to server
          const formData = new FormData();
          setIsLoadingImage(true);
          formData.append('file', file);
          fetch(`${apiUrl}/file/thumbnails/upload`, {
            method: 'POST',
            body: formData,
            credentials: 'include',
          })
            .then((res) => res.json())
            .then((response) => {
              // console.log(response.data.fileUrl);

              setFieldValue('thumbnail', response.data.fileUrl);
              setIsLoadingImage(false);
            })
            .catch(() => {
              toast.error('Error uploading thumbnail');
              setIsLoadingImage(false);
            });
        }
      };
      reader.readAsDataURL(file);
    }
  };

  const handleDeleteThumbnail = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    setIsLoadingImage(true);
    fetch(`${apiUrl}/file/thumbnails/delete?file=${values.thumbnail}`, {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json',
      },
      credentials: 'include',
    })
      .then(() => {
        setFileInputKey(new Date().getTime().toString());
        setFieldValue('thumbnail', '');
        setIsLoadingImage(false);
      })
      .catch(() => {
        setFileInputKey(new Date().getTime().toString());
        setFieldValue('thumbnail', '');
        setIsLoadingImage(false);
      });
  };

  if (isEdit && (isGetLoading || isGetTags)) return <Fallback />;
  return (
    <>
      {/* <PageTitle title="Create User" /> */}
      <div className="flex justify-center items-center">
        <div className="w-full px-6">
          <FormikProvider value={formik}>
            <Form onSubmit={handleSubmit}>
              <div className="flex justify-between items-center gap-4 py-2">
                <span className="text-slate-600 text-[25px] font-semibold">
                  {isEdit ? 'Edit' : 'Create'} Course
                </span>
                <div className="space-x-2">
                  <Button
                    variant="outline"
                    className="font-semibold font-inter rounded-md"
                    type="button"
                    onClick={(e) => {
                      const res = window.confirm(
                        'Are you sure you want to cancel?'
                      );
                      if (!isEdit && !res) {
                        return;
                      }
                      if (!isEdit && res && values.thumbnail) {
                        handleDeleteThumbnail(e);
                      }
                      navigate('/dashboard/courses/instructor');
                    }}
                  >
                    Cancel
                  </Button>
                  <Button
                    type="submit"
                    className="bg-amber-500 rounded-md font-semibold font-inter text-white"
                    isLoading={isLoading || isGetLoading || isUpdating}
                    disabled={isLoadingImage}
                  >
                    Save
                  </Button>
                </div>
              </div>
              <div className="grid grid-cols-2 gap-y-4 gap-x-6 py-5">
                {inputs.map(
                  ({ name, type = 'text', label, required, ...props }, i) => {
                    // if (isEdit && name === 'password') return null;
                    return (
                      <div
                        className={`grid gap-2 ${
                          type === 'image' && 'col-span-2'
                        }`}
                        key={i}
                      >
                        <div className="flex items-center space-x-1">
                          <Label htmlFor={name}>{label}</Label>
                          {required! && <span className="text-red-500">*</span>}
                        </div>
                        {type !== 'image' && type !== 'multiSelect' ? (
                          <Input
                            className="bg-white"
                            type={type}
                            {...props}
                            {...getFieldProps(name)}
                            error={
                              touched[name] && errors[name]
                                ? errors[name]
                                : undefined
                            }
                          />
                        ) : type !== 'multiSelect' ? (
                          <div className="space-y-1 flex flex-col">
                            <input
                              key={fileInputKey}
                              type="file"
                              accept="image/*"
                              onChange={handleFileChange}
                              ref={fileInputRef}
                              className="hidden"
                            />
                            {touched[name] && errors[name] && (
                              <p className="text-red-500 text-sm">
                                Thumbnail is required
                              </p>
                            )}
                            {values.thumbnail ? (
                              <>
                                <Button
                                  className="ml-1 w-44 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-md"
                                  onClick={handleDeleteThumbnail}
                                  isLoading={isLoadingImage}
                                >
                                  Delete
                                </Button>
                                <img
                                  draggable={false}
                                  src={values.thumbnail}
                                  alt="thumbnail"
                                  className="max-w-[700px] object-cover rounded-md"
                                />
                              </>
                            ) : (
                              <Button
                                onClick={handleButtonClick}
                                isLoading={isLoadingImage}
                                className="w-44 bg-amber-50 hover:bg-amber-100 hover:border-2 text-amber-800 rounded-md font-semibold font-inter border border-amber-100"
                                loadingSpinnerColor="amber-800"
                              >
                                <UploadCloudIcon className="h-5 w-5 mr-2" />
                                Upload an Image
                              </Button>
                            )}
                          </div>
                        ) : (
                          <CreatableSelect
                            value={values?.tags?.map((tag) => ({
                              label: tag,
                              value: tag,
                            }))}
                            isMulti
                            options={tags}
                            onChange={(value) => {
                              setFieldValue(
                                'tags',
                                value.map((v) => v.value)
                              );
                            }}
                          />
                        )}
                      </div>
                    );
                  }
                )}
              </div>
            </Form>
          </FormikProvider>
        </div>
      </div>
    </>
  );
}
const inputs: InputRequiredFields<CEMarketCourseI>[] = [
  {
    required: true,
    name: 'title',
    placeholder: 'Enter the course title',
    type: 'text',
    id: 'title',
    label: 'Title',
    autoFocus: true,
  },
  {
    required: true,
    name: 'description',
    placeholder: 'Enter the course description',
    type: 'text',
    id: 'description',
    label: 'Description',
  },
  {
    required: true,
    name: 'price',
    placeholder: 'Enter the course price',
    type: 'number',
    id: 'price',
    label: 'Price',
  },
  {
    required: false,
    name: 'tags',
    placeholder: 'Enter the course tags',
    type: 'multiSelect',
    id: 'tags',
    label: 'Tags',
  },
  {
    required: false,
    name: 'videoThumbnail',
    placeholder: 'Enter youtube link',
    type: 'text',
    id: 'videoThumbnail',
    label: 'Video Thumbnail',
  },
  {
    required: true,
    name: 'totalHours',
    placeholder: 'Enter the course total hours',
    type: 'number',
    id: 'totalHours',
    label: 'Total Hours',
  },
  {
    required: true,
    name: 'chaptersCount',
    placeholder: 'Enter the course chapters count',
    type: 'number',
    id: 'chaptersCount',
    label: 'Chapters Count',
  },
  {
    required: true,
    name: 'thumbnail',
    placeholder: 'Upload a thumbnail',
    type: 'image',
    id: 'thumbnail',
    label: 'Thumbnail',
  },
];
export default CreateCourseByInst;