mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: refactor & improve course edit page
This commit is contained in:
parent
c7061a26fa
commit
ccb18107ea
6 changed files with 492 additions and 348 deletions
|
|
@ -0,0 +1,122 @@
|
|||
"use client";
|
||||
import React, { FC, use, useEffect, useReducer } from 'react'
|
||||
import { swrFetcher } from "@services/utils/ts/requests";
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
||||
import useSWR from 'swr';
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||
import Link from 'next/link';
|
||||
import CourseEdition from '../subpages/CourseEdition';
|
||||
import CourseContentEdition from '../subpages/CourseContentEdition';
|
||||
import ErrorUI from '@components/StyledElements/Error/Error';
|
||||
import { updateChaptersMetadata } from '@services/courses/chapters';
|
||||
import { Check, SaveAllIcon, Timer } from 'lucide-react';
|
||||
|
||||
function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) {
|
||||
const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/meta/course_${courseid}`, swrFetcher);
|
||||
const { data: course_meta, error: course_meta_error, isLoading: course_meta_isloading } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher);
|
||||
const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {});
|
||||
const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true);
|
||||
|
||||
|
||||
function courseChaptersReducer(state: any, action: any) {
|
||||
switch (action.type) {
|
||||
case 'updated_chapter':
|
||||
// action will contain the entire state, just update the entire state
|
||||
return action.payload;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
function savedContentReducer(state: any, action: any) {
|
||||
switch (action.type) {
|
||||
case 'saved_content':
|
||||
return true;
|
||||
case 'unsaved_content':
|
||||
return false;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
function saveCourse() {
|
||||
if (subpage.toString() === 'content') {
|
||||
updateChaptersMetadata(courseid, courseChaptersMetadata)
|
||||
dispatchSavedContent({ type: 'saved_content' })
|
||||
}
|
||||
else if (subpage.toString() === 'general') {
|
||||
console.log('general')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (chapters_meta) {
|
||||
dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta })
|
||||
dispatchSavedContent({ type: 'saved_content' })
|
||||
}
|
||||
}, [chapters_meta])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
|
||||
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
|
||||
{course_meta_isloading && <div className='text-sm text-gray-500'>Loading...</div>}
|
||||
{course_meta && <>
|
||||
<div className='flex items-center'><div className='info flex space-x-5 items-center grow'>
|
||||
<div className='flex'>
|
||||
<Link href={getUriWithOrg(course_meta.course.orgslug, "") + `/course/${courseid}`}>
|
||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course_meta.course.org_id, course_meta.course.course_id, course_meta.course.thumbnail)}`} alt="" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col ">
|
||||
<div className='text-sm text-gray-500'>Edit Course</div>
|
||||
<div className='text-2xl font-bold first-letter:uppercase'>{course_meta.course.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-5 items-center'>
|
||||
{savedContent ? <></> : <div className='text-gray-600 flex space-x-2 items-center antialiased'>
|
||||
<Timer size={15} />
|
||||
<div>
|
||||
Unsaved changes
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
<div className={`' px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` + (savedContent ? 'bg-gray-600 text-white' : 'bg-black text-white border hover:bg-gray-900 ')
|
||||
} onClick={saveCourse}>
|
||||
|
||||
{savedContent ? <Check size={20} /> : <SaveAllIcon size={20} />}
|
||||
{savedContent ? <div className=''>Saved</div> : <div className=''>Save</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
<div className='flex space-x-5 pt-3 font-black text-sm'>
|
||||
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/general`}>
|
||||
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>General</div>
|
||||
</Link>
|
||||
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/content`}>
|
||||
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>Content</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CoursePageViewer dispatchSavedContent={dispatchSavedContent} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} />
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any }) => {
|
||||
if (subpage.toString() === 'general') {
|
||||
return <CourseEdition />
|
||||
}
|
||||
else if (subpage.toString() === 'content') {
|
||||
return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} />
|
||||
}
|
||||
else {
|
||||
return <ErrorUI />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CourseEditClient
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import CourseEditClient from "./edit";
|
||||
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
||||
import { cookies } from "next/headers";
|
||||
import { Metadata } from 'next';
|
||||
|
||||
type MetadataProps = {
|
||||
params: { orgslug: string, courseid: string };
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
};
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: MetadataProps,
|
||||
): Promise<Metadata> {
|
||||
const cookieStore = cookies();
|
||||
const access_token_cookie: any = cookieStore.get('access_token_cookie');
|
||||
|
||||
|
||||
// Get Org context information
|
||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
|
||||
|
||||
return {
|
||||
title: `Edit Course - ` + course_meta.course.name,
|
||||
description: course_meta.course.mini_description,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function CourseEdit(params: any) {
|
||||
let subpage = params.params.subpage ? params.params.subpage : 'general';
|
||||
return (
|
||||
<>
|
||||
<CourseEditClient params={params} subpage={subpage} courseid={params.params.courseid} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default CourseEdit;
|
||||
|
|
@ -1,346 +0,0 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
import { initialData, initialData2 } from "@components/Pages/CourseEdit/Draggables/data";
|
||||
import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
|
||||
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
|
||||
import { useRouter } from "next/navigation";
|
||||
import NewChapterModal from "@components/Objects/Modals/Chapters/NewChapter";
|
||||
import NewActivityModal from "@components/Objects/Modals/Activities/Create/NewActivity";
|
||||
import { createActivity, createFileActivity, createExternalVideoActivity } from "@services/courses/activities";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import Modal from "@components/StyledElements/Modal/Modal";
|
||||
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
|
||||
import { Folders, Package2, SaveIcon } from "lucide-react";
|
||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||
import { revalidateTags } from "@services/utils/ts/requests";
|
||||
|
||||
function CourseEdit(params: any) {
|
||||
|
||||
const router = useRouter();
|
||||
// Initial Course State
|
||||
const [data, setData] = useState(initialData2) as any;
|
||||
|
||||
// New Chapter Modal State
|
||||
const [newChapterModal, setNewChapterModal] = useState(false) as any;
|
||||
// New Activity Modal State
|
||||
const [newActivityModal, setNewActivityModal] = useState(false) as any;
|
||||
const [newActivityModalData, setNewActivityModalData] = useState("") as any;
|
||||
|
||||
// Check window availability
|
||||
const [winReady, setwinReady] = useState(false);
|
||||
const courseid = params.params.courseid;
|
||||
const orgslug = params.params.orgslug;
|
||||
|
||||
async function getCourseChapters() {
|
||||
try {
|
||||
const courseChapters = await getCourseChaptersMetadata(courseid, { revalidate: 120 });
|
||||
setData(courseChapters);
|
||||
} catch (error: any) {
|
||||
denyAccessToUser(error, router)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (courseid && orgslug) {
|
||||
getCourseChapters();
|
||||
}
|
||||
|
||||
setwinReady(true);
|
||||
}, [courseid, orgslug]);
|
||||
|
||||
// get a list of chapters order by chapter order
|
||||
const getChapters = () => {
|
||||
const chapterOrder = data.chapterOrder ? data.chapterOrder : [];
|
||||
return chapterOrder.map((chapterId: any) => {
|
||||
const chapter = data.chapters[chapterId];
|
||||
let activities = [];
|
||||
if (data.activities) {
|
||||
activities = chapter.activityIds.map((activityId: any) => data.activities[activityId])
|
||||
? chapter.activityIds.map((activityId: any) => data.activities[activityId])
|
||||
: [];
|
||||
}
|
||||
return {
|
||||
list: {
|
||||
chapter: chapter,
|
||||
activities: activities,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Submit new chapter
|
||||
const submitChapter = async (chapter: any) => {
|
||||
await createChapter(chapter, courseid);
|
||||
await getCourseChapters();
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
setNewChapterModal(false);
|
||||
};
|
||||
|
||||
// Submit new activity
|
||||
const submitActivity = async (activity: any) => {
|
||||
|
||||
let org = await getOrganizationContextInfo(orgslug, { revalidate: 1800 });
|
||||
await updateChaptersMetadata(courseid, data);
|
||||
await createActivity(activity, activity.chapterId, org.org_id);
|
||||
await getCourseChapters();
|
||||
setNewActivityModal(false);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
// Submit File Upload
|
||||
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
|
||||
await updateChaptersMetadata(courseid, data);
|
||||
await createFileActivity(file, type, activity, chapterId);
|
||||
await getCourseChapters();
|
||||
setNewActivityModal(false);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
// Submit YouTube Video Upload
|
||||
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
|
||||
|
||||
await updateChaptersMetadata(courseid, data);
|
||||
await createExternalVideoActivity(external_video_data, activity, chapterId);
|
||||
await getCourseChapters();
|
||||
setNewActivityModal(false);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const deleteChapterUI = async (chapterId: any) => {
|
||||
|
||||
await deleteChapter(chapterId);
|
||||
getCourseChapters();
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const updateChapters = () => {
|
||||
|
||||
updateChaptersMetadata(courseid, data);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
/*
|
||||
Modals
|
||||
*/
|
||||
|
||||
const openNewActivityModal = async (chapterId: any) => {
|
||||
|
||||
setNewActivityModal(true);
|
||||
setNewActivityModalData(chapterId);
|
||||
};
|
||||
|
||||
// Close new chapter modal
|
||||
const closeNewChapterModal = () => {
|
||||
setNewChapterModal(false);
|
||||
};
|
||||
|
||||
const closeNewActivityModal = () => {
|
||||
|
||||
|
||||
setNewActivityModal(false);
|
||||
};
|
||||
|
||||
/*
|
||||
Drag and drop functions
|
||||
|
||||
*/
|
||||
const onDragEnd = (result: any) => {
|
||||
const { destination, source, draggableId, type } = result;
|
||||
|
||||
|
||||
// check if the activity is dropped outside the droppable area
|
||||
if (!destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the activity is dropped in the same place
|
||||
if (destination.droppableId === source.droppableId && destination.index === source.index) {
|
||||
return;
|
||||
}
|
||||
//////////////////////////// CHAPTERS ////////////////////////////
|
||||
if (type === "chapter") {
|
||||
const newChapterOrder = Array.from(data.chapterOrder);
|
||||
newChapterOrder.splice(source.index, 1);
|
||||
newChapterOrder.splice(destination.index, 0, draggableId);
|
||||
|
||||
const newState = {
|
||||
...data,
|
||||
chapterOrder: newChapterOrder,
|
||||
};
|
||||
|
||||
|
||||
setData(newState);
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////// ACTIVITIES IN SAME CHAPTERS ////////////////////////////
|
||||
// check if the activity is dropped in the same chapter
|
||||
const start = data.chapters[source.droppableId];
|
||||
const finish = data.chapters[destination.droppableId];
|
||||
|
||||
// check if the activity is dropped in the same chapter
|
||||
if (start === finish) {
|
||||
// create new arrays for chapters and activities
|
||||
const chapter = data.chapters[source.droppableId];
|
||||
const newActivityIds = Array.from(chapter.activityIds);
|
||||
|
||||
// remove the activity from the old position
|
||||
newActivityIds.splice(source.index, 1);
|
||||
|
||||
// add the activity to the new position
|
||||
newActivityIds.splice(destination.index, 0, draggableId);
|
||||
|
||||
const newChapter = {
|
||||
...chapter,
|
||||
activityIds: newActivityIds,
|
||||
};
|
||||
|
||||
const newState = {
|
||||
...data,
|
||||
chapters: {
|
||||
...data.chapters,
|
||||
[newChapter.id]: newChapter,
|
||||
},
|
||||
};
|
||||
|
||||
setData(newState);
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////// ACTIVITIES IN DIFF CHAPTERS ////////////////////////////
|
||||
// check if the activity is dropped in a different chapter
|
||||
if (start !== finish) {
|
||||
// create new arrays for chapters and activities
|
||||
const startChapterActivityIds = Array.from(start.activityIds);
|
||||
|
||||
// remove the activity from the old position
|
||||
startChapterActivityIds.splice(source.index, 1);
|
||||
const newStart = {
|
||||
...start,
|
||||
activityIds: startChapterActivityIds,
|
||||
};
|
||||
|
||||
// add the activity to the new position within the chapter
|
||||
const finishChapterActivityIds = Array.from(finish.activityIds);
|
||||
finishChapterActivityIds.splice(destination.index, 0, draggableId);
|
||||
const newFinish = {
|
||||
...finish,
|
||||
activityIds: finishChapterActivityIds,
|
||||
};
|
||||
|
||||
const newState = {
|
||||
...data,
|
||||
chapters: {
|
||||
...data.chapters,
|
||||
[newStart.id]: newStart,
|
||||
[newFinish.id]: newFinish,
|
||||
},
|
||||
};
|
||||
|
||||
setData(newState);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
>
|
||||
<GeneralWrapperStyled>
|
||||
<div className="font-bold text-lg flex space-x-2 items-center">
|
||||
<p> Edit Course {" "}</p>
|
||||
|
||||
|
||||
<div
|
||||
className="bg-black z-20 hover:bg-gray-950 text-white font-bold p-1 px-2 text-sm rounded flex items-center cursor-pointer space-x-2 hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
updateChapters();
|
||||
}}
|
||||
>
|
||||
<SaveIcon className="w-4 h-4" />
|
||||
<p>Save</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isDialogOpen={newActivityModal}
|
||||
onOpenChange={setNewActivityModal}
|
||||
minHeight="no-min"
|
||||
addDefCloseButton={false}
|
||||
dialogContent={<NewActivityModal
|
||||
closeModal={closeNewActivityModal}
|
||||
submitFileActivity={submitFileActivity}
|
||||
submitExternalVideo={submitExternalVideo}
|
||||
submitActivity={submitActivity}
|
||||
chapterId={newActivityModalData}
|
||||
></NewActivityModal>}
|
||||
dialogTitle="Create Activity"
|
||||
dialogDescription="Choose between types of activities to add to the course"
|
||||
|
||||
/>
|
||||
|
||||
<br />
|
||||
{winReady && (
|
||||
<div className="flex flex-col max-w-7xl justify-center items-center mx-auto">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable key="chapters" droppableId="chapters" type="chapter">
|
||||
{(provided) => (
|
||||
<>
|
||||
<div key={"chapters"} {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{getChapters().map((info: any, index: any) => (
|
||||
<>
|
||||
<Chapter
|
||||
orgslug={orgslug}
|
||||
courseid={courseid}
|
||||
openNewActivityModal={openNewActivityModal}
|
||||
deleteChapter={deleteChapterUI}
|
||||
key={index}
|
||||
info={info}
|
||||
index={index}
|
||||
></Chapter>
|
||||
</>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<Modal
|
||||
isDialogOpen={newChapterModal}
|
||||
onOpenChange={setNewChapterModal}
|
||||
minHeight="sm"
|
||||
dialogContent={<NewChapterModal
|
||||
closeModal={closeNewChapterModal}
|
||||
submitChapter={submitChapter}
|
||||
></NewChapterModal>}
|
||||
dialogTitle="Create chapter"
|
||||
dialogDescription="Add a new chapter to the course"
|
||||
dialogTrigger={
|
||||
<div className="flex max-w-7xl bg-black shadow rounded-md text-white justify-center space-x-2 p-3 w-72 hover:bg-gray-900 hover:cursor-pointer">
|
||||
<Folders size={20} />
|
||||
<div>Add chapter +</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</GeneralWrapperStyled >
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default CourseEdit;
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
|
||||
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
|
||||
import { useRouter } from "next/navigation";
|
||||
import NewChapterModal from "@components/Objects/Modals/Chapters/NewChapter";
|
||||
import NewActivityModal from "@components/Objects/Modals/Activities/Create/NewActivity";
|
||||
import { createActivity, createFileActivity, createExternalVideoActivity } from "@services/courses/activities";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import Modal from "@components/StyledElements/Modal/Modal";
|
||||
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
|
||||
import { Folders, SaveIcon } from "lucide-react";
|
||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
||||
import { mutate } from "swr";
|
||||
import { getAPIUrl } from "@services/config/config";
|
||||
|
||||
function CourseContentEdition(props: any) {
|
||||
const router = useRouter();
|
||||
// Initial Course State
|
||||
const data = props.data;
|
||||
|
||||
// New Chapter Modal State
|
||||
const [newChapterModal, setNewChapterModal] = useState(false) as any;
|
||||
// New Activity Modal State
|
||||
const [newActivityModal, setNewActivityModal] = useState(false) as any;
|
||||
const [newActivityModalData, setNewActivityModalData] = useState("") as any;
|
||||
|
||||
// Check window availability
|
||||
const [winReady, setwinReady] = useState(false);
|
||||
const courseid = props.courseid;
|
||||
const orgslug = props.orgslug;
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setwinReady(true);
|
||||
}, [courseid, orgslug]);
|
||||
|
||||
// get a list of chapters order by chapter order
|
||||
const getChapters = () => {
|
||||
const chapterOrder = data.chapterOrder ? data.chapterOrder : [];
|
||||
return chapterOrder.map((chapterId: any) => {
|
||||
const chapter = data.chapters[chapterId];
|
||||
let activities = [];
|
||||
if (data.activities) {
|
||||
activities = chapter.activityIds.map((activityId: any) => data.activities[activityId])
|
||||
? chapter.activityIds.map((activityId: any) => data.activities[activityId])
|
||||
: [];
|
||||
}
|
||||
return {
|
||||
list: {
|
||||
chapter: chapter,
|
||||
activities: activities,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Submit new chapter
|
||||
const submitChapter = async (chapter: any) => {
|
||||
await createChapter(chapter, courseid);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
|
||||
// await getCourseChapters();
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
setNewChapterModal(false);
|
||||
};
|
||||
|
||||
// Submit new activity
|
||||
const submitActivity = async (activity: any) => {
|
||||
let org = await getOrganizationContextInfo(orgslug, { revalidate: 1800 });
|
||||
await updateChaptersMetadata(courseid, data);
|
||||
await createActivity(activity, activity.chapterId, org.org_id);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
|
||||
// await getCourseChapters();
|
||||
setNewActivityModal(false);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
// Submit File Upload
|
||||
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
|
||||
await updateChaptersMetadata(courseid, data);
|
||||
await createFileActivity(file, type, activity, chapterId);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
|
||||
// await getCourseChapters();
|
||||
setNewActivityModal(false);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
// Submit YouTube Video Upload
|
||||
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
|
||||
await updateChaptersMetadata(courseid, data);
|
||||
await createExternalVideoActivity(external_video_data, activity, chapterId);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
|
||||
// await getCourseChapters();
|
||||
setNewActivityModal(false);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const deleteChapterUI = async (chapterId: any) => {
|
||||
|
||||
await deleteChapter(chapterId);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
|
||||
// await getCourseChapters();
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const updateChapters = () => {
|
||||
updateChaptersMetadata(courseid, data);
|
||||
revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
/*
|
||||
Modals
|
||||
*/
|
||||
|
||||
const openNewActivityModal = async (chapterId: any) => {
|
||||
setNewActivityModal(true);
|
||||
setNewActivityModalData(chapterId);
|
||||
};
|
||||
|
||||
// Close new chapter modal
|
||||
const closeNewChapterModal = () => {
|
||||
setNewChapterModal(false);
|
||||
};
|
||||
|
||||
const closeNewActivityModal = () => {
|
||||
setNewActivityModal(false);
|
||||
};
|
||||
|
||||
/*
|
||||
Drag and drop functions
|
||||
|
||||
*/
|
||||
const onDragEnd = async (result: any) => {
|
||||
const { destination, source, draggableId, type } = result;
|
||||
|
||||
|
||||
// check if the activity is dropped outside the droppable area
|
||||
if (!destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the activity is dropped in the same place
|
||||
if (destination.droppableId === source.droppableId && destination.index === source.index) {
|
||||
return;
|
||||
}
|
||||
//////////////////////////// CHAPTERS ////////////////////////////
|
||||
if (type === "chapter") {
|
||||
const newChapterOrder = Array.from(data.chapterOrder);
|
||||
newChapterOrder.splice(source.index, 1);
|
||||
newChapterOrder.splice(destination.index, 0, draggableId);
|
||||
|
||||
const newState = {
|
||||
...data,
|
||||
chapterOrder: newChapterOrder,
|
||||
};
|
||||
|
||||
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
|
||||
props.dispatchSavedContent({ type: 'unsaved_content' })
|
||||
//setData(newState);
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////// ACTIVITIES IN SAME CHAPTERS ////////////////////////////
|
||||
// check if the activity is dropped in the same chapter
|
||||
const start = data.chapters[source.droppableId];
|
||||
const finish = data.chapters[destination.droppableId];
|
||||
|
||||
// check if the activity is dropped in the same chapter
|
||||
if (start === finish) {
|
||||
// create new arrays for chapters and activities
|
||||
const chapter = data.chapters[source.droppableId];
|
||||
const newActivityIds = Array.from(chapter.activityIds);
|
||||
|
||||
// remove the activity from the old position
|
||||
newActivityIds.splice(source.index, 1);
|
||||
|
||||
// add the activity to the new position
|
||||
newActivityIds.splice(destination.index, 0, draggableId);
|
||||
|
||||
const newChapter = {
|
||||
...chapter,
|
||||
activityIds: newActivityIds,
|
||||
};
|
||||
|
||||
const newState = {
|
||||
...data,
|
||||
chapters: {
|
||||
...data.chapters,
|
||||
[newChapter.id]: newChapter,
|
||||
},
|
||||
};
|
||||
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
|
||||
props.dispatchSavedContent({ type: 'unsaved_content' })
|
||||
//setData(newState);
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////// ACTIVITIES IN DIFF CHAPTERS ////////////////////////////
|
||||
// check if the activity is dropped in a different chapter
|
||||
if (start !== finish) {
|
||||
// create new arrays for chapters and activities
|
||||
const startChapterActivityIds = Array.from(start.activityIds);
|
||||
|
||||
// remove the activity from the old position
|
||||
startChapterActivityIds.splice(source.index, 1);
|
||||
const newStart = {
|
||||
...start,
|
||||
activityIds: startChapterActivityIds,
|
||||
};
|
||||
|
||||
// add the activity to the new position within the chapter
|
||||
const finishChapterActivityIds = Array.from(finish.activityIds);
|
||||
finishChapterActivityIds.splice(destination.index, 0, draggableId);
|
||||
const newFinish = {
|
||||
...finish,
|
||||
activityIds: finishChapterActivityIds,
|
||||
};
|
||||
|
||||
const newState = {
|
||||
...data,
|
||||
chapters: {
|
||||
...data.chapters,
|
||||
[newStart.id]: newStart,
|
||||
[newFinish.id]: newFinish,
|
||||
},
|
||||
};
|
||||
|
||||
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
|
||||
props.dispatchSavedContent({ type: 'unsaved_content' })
|
||||
//setData(newState);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=""
|
||||
>
|
||||
<GeneralWrapperStyled>
|
||||
<Modal
|
||||
isDialogOpen={newActivityModal}
|
||||
onOpenChange={setNewActivityModal}
|
||||
minHeight="no-min"
|
||||
addDefCloseButton={false}
|
||||
dialogContent={<NewActivityModal
|
||||
closeModal={closeNewActivityModal}
|
||||
submitFileActivity={submitFileActivity}
|
||||
submitExternalVideo={submitExternalVideo}
|
||||
submitActivity={submitActivity}
|
||||
chapterId={newActivityModalData}
|
||||
></NewActivityModal>}
|
||||
dialogTitle="Create Activity"
|
||||
dialogDescription="Choose between types of activities to add to the course"
|
||||
|
||||
/>
|
||||
{winReady && (
|
||||
<div className="flex flex-col">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable key="chapters" droppableId="chapters" type="chapter">
|
||||
{(provided) => (
|
||||
<>
|
||||
<div key={"chapters"} {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{getChapters().map((info: any, index: any) => (
|
||||
<>
|
||||
<Chapter
|
||||
orgslug={orgslug}
|
||||
courseid={courseid}
|
||||
openNewActivityModal={openNewActivityModal}
|
||||
deleteChapter={deleteChapterUI}
|
||||
key={index}
|
||||
info={info}
|
||||
index={index}
|
||||
></Chapter>
|
||||
</>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<Modal
|
||||
isDialogOpen={newChapterModal}
|
||||
onOpenChange={setNewChapterModal}
|
||||
minHeight="sm"
|
||||
dialogContent={<NewChapterModal
|
||||
closeModal={closeNewChapterModal}
|
||||
submitChapter={submitChapter}
|
||||
></NewChapterModal>}
|
||||
dialogTitle="Create chapter"
|
||||
dialogDescription="Add a new chapter to the course"
|
||||
dialogTrigger={
|
||||
<div className="flex max-w-7xl bg-black text-sm shadow rounded-md items-center text-white justify-center mx-auto space-x-2 p-3 w-72 hover:bg-gray-900 hover:cursor-pointer">
|
||||
<Folders size={16} />
|
||||
<div>Add chapter +</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</GeneralWrapperStyled >
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default CourseContentEdition;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
|
||||
function CourseEdition() {
|
||||
return (
|
||||
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
|
||||
Course Edition
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CourseEdition
|
||||
Loading…
Add table
Add a link
Reference in a new issue