diff --git a/Dockerfile b/Dockerfile index f42ea0a1..419c6c96 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,4 @@ RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt COPY ./ /usr/learnhouse # -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" ] +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" , "--reload" ] diff --git a/app.py b/app.py index 2801bcc6..3a7f62c6 100644 --- a/app.py +++ b/app.py @@ -8,8 +8,6 @@ from fastapi.staticfiles import StaticFiles from fastapi_jwt_auth.exceptions import AuthJWTException from fastapi.middleware.gzip import GZipMiddleware -from src.security.rbac.rbac import authorization_verify_based_on_roles, authorization_verify_if_element_is_public, authorization_verify_if_user_is_author -from src.services.users.schemas.users import UserRolesInOrganization # from src.services.mocks.initial import create_initial_data diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx index 732c4068..d55e87c7 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx @@ -15,7 +15,7 @@ const CollectionAdminEditsArea = (props: any) => { const deleteCollectionUI = async (collectionId: number) => { await deleteCollection(collectionId); - revalidateTags(["collections"], props.orgslug); + await revalidateTags(["collections"], props.orgslug); // reload the page router.refresh(); router.push(getUriWithOrg(props.orgslug, "/collections")); diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx index abee6674..e338f86e 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx @@ -44,11 +44,10 @@ function NewCollection(params: any) { org_id: org.org_id, }; await createCollection(collection); - revalidateTags(["collections"], orgslug); + await revalidateTags(["collections"], orgslug); + router.refresh(); router.prefetch(getUriWithOrg(orgslug, "/collections")); router.push(getUriWithOrg(orgslug, "/collections")); - router.refresh(); - }; diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx index 0524577b..37d505dd 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx @@ -39,7 +39,7 @@ const CollectionsPage = async (params: any) => { const orgslug = params.params.orgslug; const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org_id = org.org_id; - const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token_cookie ? access_token_cookie.value : null); + const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] }); return ( @@ -54,7 +54,7 @@ const CollectionsPage = async (params: any) => {
{collections.map((collection: any) => (
- +

{collection.name}

diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx index 95d1149c..a39c6865 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx @@ -23,7 +23,7 @@ const CourseClient = (props: any) => { async function startCourseUI() { // Create activity await startCourse("course_" + courseid, orgslug); - revalidateTags(['courses'], orgslug); + await revalidateTags(['courses'], orgslug); router.refresh(); // refresh page (FIX for Next.js BUG) @@ -34,7 +34,7 @@ const CourseClient = (props: any) => { // Close activity let activity = await removeCourse("course_" + courseid, orgslug); // Mutate course - revalidateTags(['courses'], orgslug); + await revalidateTags(['courses'], orgslug); router.refresh(); // refresh page (FIX for Next.js BUG) 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..ed292f48 --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx @@ -0,0 +1,152 @@ +"use client"; +import React, { FC, use, useEffect, useReducer } from 'react' +import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; +import { getAPIUrl, getUriWithOrg } from '@services/config/config'; +import useSWR, { mutate } 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'; +import Loading from '../../loading'; +import { updateCourse } from '@services/courses/courses'; +import { useRouter } from 'next/navigation'; + +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, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseid}`, swrFetcher); + const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {}); + const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {}); + const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true); + const router = useRouter(); + + + + 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 courseReducer(state: any, action: any) { + switch (action.type) { + case 'updated_course': + // 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(); + } + } + + async function saveCourse() { + if (subpage.toString() === 'content') { + await updateChaptersMetadata(courseid, courseChaptersMetadata) + dispatchSavedContent({ type: 'saved_content' }) + await mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`) + await revalidateTags(['courses'], params.params.orgslug) + router.refresh() + } + else if (subpage.toString() === 'general') { + await updateCourse(courseid, courseState) + dispatchSavedContent({ type: 'saved_content' }) + await mutate(`${getAPIUrl()}courses/course_${courseid}`) + await revalidateTags(['courses'], params.params.orgslug) + router.refresh() + } + } + + useEffect(() => { + if (chapters_meta) { + dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta }) + dispatchSavedContent({ type: 'saved_content' }) + } + if (course) { + dispatchCourseMetadata({ type: 'updated_course', payload: course }) + dispatchSavedContent({ type: 'saved_content' }) + } + }, [chapters_meta, course]) + + return ( + <> +
+
+ {course_isloading &&
Loading...
} + {course && <> +
+
+ + + +
+
+
Edit Course
+
{course.name}
+
+
+
+ {savedContent ? <> :
+ +
+ Unsaved changes +
+ +
} +
+ + {savedContent ? : } + {savedContent ?
Saved
:
Save
} +
+
+
+ } +
+ +
General
+ + +
Content
+ +
+
+
+ + + + ) +} + +const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch, dispatchCourseMetadata: React.Dispatch, dispatchSavedContent: React.Dispatch, courseChaptersMetadata: any, courseState: any }) => { + if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) { + return + } + else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) { + return + } + else if (subpage.toString() === 'content' || subpage.toString() === 'general') { + 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..81b3adb9 --- /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(); + await 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); + await 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); + await 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); + await revalidateTags(['courses'], orgslug); + router.refresh(); + }; + + const deleteChapterUI = async (chapterId: any) => { + + await deleteChapter(chapterId); + mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + // await getCourseChapters(); + await 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..23b77f85 --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx @@ -0,0 +1,116 @@ +"use client"; +import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input, Textarea } from '@components/StyledElements/Form/Form' +import * as Form from '@radix-ui/react-form'; +import { useFormik } from 'formik'; +import { AlertTriangle } from "lucide-react"; +import React from "react"; + +const validate = (values: any) => { + const errors: any = {}; + + if (!values.name) { + errors.name = 'Required'; + } + + if (values.name.length > 100) { + errors.name = 'Must be 80 characters or less'; + } + + if (!values.mini_description) { + errors.mini_description = 'Required'; + } + + if (values.mini_description.length > 200) { + errors.mini_description = 'Must be 200 characters or less'; + } + + if (!values.description) { + errors.description = 'Required'; + + } + + if (values.description.length > 1000) { + errors.description = 'Must be 1000 characters or less'; + } + + + if (!values.learnings) { + errors.learnings = 'Required'; + } + + return errors; +}; + +function CourseEdition(props: any) { + const [error, setError] = React.useState(''); + const formik = useFormik({ + initialValues: { + name: String(props.data.name), + mini_description: String(props.data.mini_description), + description: String(props.data.description), + learnings: String(props.data.learnings), + }, + validate, + onSubmit: async values => { + }, + }); + + + React.useEffect(() => { + // This code will run whenever form values are updated + if (formik.values !== formik.initialValues) { + props.dispatchSavedContent({ type: 'unsaved_content' }); + const updatedCourse = { + ...props.data, + name: formik.values.name, + mini_description: formik.values.mini_description, + description: formik.values.description, + learnings: formik.values.learnings.split(", "), + }; + props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse }); + } + }, [formik.values, formik.initialValues]); + + + return ( +
+
+ {error && ( +
+ +
{error}
+
+ )} + + + + + + + + + + + + + + + + +