From ccb18107ea40a2b47f2427b0c985f813a2f37d42 Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 19 Aug 2023 15:58:29 +0200 Subject: [PATCH] feat: refactor & improve course edit page --- .../[courseid]/edit/[[...subpage]]/edit.tsx | 122 ++++++ .../[courseid]/edit/[[...subpage]]/page.tsx | 40 ++ .../course/[courseid]/edit/page.tsx | 346 ------------------ .../edit/subpages/CourseContentEdition.tsx | 318 ++++++++++++++++ .../edit/subpages/CourseEdition.tsx | 11 + .../Pages/CourseEdit/Draggables/Chapter.tsx | 3 +- 6 files changed, 492 insertions(+), 348 deletions(-) create mode 100644 front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx create mode 100644 front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx delete mode 100644 front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx create mode 100644 front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx create mode 100644 front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx new file mode 100644 index 00000000..2a12fea7 --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx @@ -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 ( + <> +
+
+ {course_meta_isloading &&
Loading...
} + {course_meta && <> +
+
+ + + +
+
+
Edit Course
+
{course_meta.course.name}
+
+
+
+ {savedContent ? <> :
+ +
+ Unsaved changes +
+ +
} +
+ + {savedContent ? : } + {savedContent ?
Saved
:
Save
} +
+
+
+ } +
+ +
General
+ + +
Content
+ +
+
+
+ + + + ) +} + +const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch, dispatchSavedContent: React.Dispatch, courseChaptersMetadata: any }) => { + if (subpage.toString() === 'general') { + return + } + else if (subpage.toString() === 'content') { + return + } + else { + return + } + +} + +export default CourseEditClient \ No newline at end of file diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx new file mode 100644 index 00000000..4463ca50 --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx @@ -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 { + 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 ( + <> + + + ); +} + + +export default CourseEdit; diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx deleted file mode 100644 index b5a46b18..00000000 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx +++ /dev/null @@ -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 ( - <> -
- -
-

Edit Course {" "}

- - -
{ - updateChapters(); - }} - > - -

Save

- -
-
- - } - dialogTitle="Create Activity" - dialogDescription="Choose between types of activities to add to the course" - - /> - -
- {winReady && ( -
- - - {(provided) => ( - <> -
- {getChapters().map((info: any, index: any) => ( - <> - - - ))} - {provided.placeholder} -
- - )} -
-
- } - dialogTitle="Create chapter" - dialogDescription="Add a new chapter to the course" - dialogTrigger={ -
- -
Add chapter +
-
- } - /> -
- )} -
-
- - ); -} - - -export default CourseEdit; diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx new file mode 100644 index 00000000..41c1f27f --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx @@ -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 ( + <> +
+ + } + dialogTitle="Create Activity" + dialogDescription="Choose between types of activities to add to the course" + + /> + {winReady && ( +
+ + + {(provided) => ( + <> +
+ {getChapters().map((info: any, index: any) => ( + <> + + + ))} + {provided.placeholder} +
+ + )} +
+
+ } + dialogTitle="Create chapter" + dialogDescription="Add a new chapter to the course" + dialogTrigger={ +
+ +
Add chapter +
+
+ } + /> +
+ )} +
+
+ + ); +} + + +export default CourseContentEdition; \ No newline at end of file diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx new file mode 100644 index 00000000..a5c6d2ac --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +function CourseEdition() { + return ( +
+ Course Edition +
+ ) +} + +export default CourseEdition \ No newline at end of file diff --git a/front/components/Pages/CourseEdit/Draggables/Chapter.tsx b/front/components/Pages/CourseEdit/Draggables/Chapter.tsx index 8232f4ac..e4e20c95 100644 --- a/front/components/Pages/CourseEdit/Draggables/Chapter.tsx +++ b/front/components/Pages/CourseEdit/Draggables/Chapter.tsx @@ -14,7 +14,7 @@ function Chapter(props: any) { {...provided.draggableProps} ref={provided.innerRef} // isDragging={snapshot.isDragging} - className="" + className="max-w-screen-2xl mx-auto" key={props.info.list.chapter.id} >

@@ -64,7 +64,6 @@ const ChapterWrapper = styled.div` margin-bottom: 20px; padding: 4px; background-color: #ffffff9d; - width: 900px; font-size: 15px; display: block; border-radius: 9px;