feat: init new edit course page

This commit is contained in:
swve 2023-12-13 09:56:11 +01:00
parent 187f75e583
commit 8d35085908
28 changed files with 891 additions and 159 deletions

View file

@ -0,0 +1,59 @@
'use client';
import EditCourseStructure from '../../../../../../../../components/Dashboard/EditCourseStructure/EditCourseStructure'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import PageLoading from '@components/Objects/Loaders/PageLoading';
import ClientComponentSkeleton from '@components/Utils/ClientComp';
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import React, { createContext, use, useEffect, useState } from 'react'
import useSWR from 'swr';
import { CourseProvider, useCourse } from '../../../../../../../../components/Dashboard/CourseContext';
import SaveState from '@components/Dashboard/UI/SaveState';
import Link from 'next/link';
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
export type CourseOverviewParams = {
orgslug: string,
courseuuid: string,
subpage: string
}
export const CourseStructureContext = createContext({}) as any;
function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
function getEntireCourseUUID(courseuuid: string) {
// add course_ to uuid
return `course_${courseuuid}`
}
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<CourseOverviewTop params={params} />
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>General</div>
</Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/structure`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${params.subpage.toString() === 'structure' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>Structure</div>
</Link>
</div>
</div>
<div className='h-6'></div>
{params.subpage == 'structure' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
</CourseProvider>
</div>
)
}
export default CourseOverviewPage

View file

@ -0,0 +1,22 @@
'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import Link from 'next/link'
import React from 'react'
function CoursesHome() {
return (
<div>
<div className='h-full w-fullbg-white'>
<div className='pl-10 tracking-tighter'>
<BreadCrumbs type='courses' />
<div className='flex pt-2'>
<div className='font-bold text-4xl'>Courses</div>
</div>
</div>
</div>
</div>
)
}
export default CoursesHome

View file

@ -0,0 +1,20 @@
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AuthProvider from '@components/Security/AuthProvider'
import React from 'react'
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
return (
<>
<AuthProvider>
<div className='flex'>
<LeftMenu/>
<div className='flex w-full'>
{children}
</div>
</div>
</AuthProvider>
</>
)
}
export default DashboardLayout

View file

@ -0,0 +1,9 @@
import React from 'react'
function DashboardHome() {
return (
<div>DashboardHome</div>
)
}
export default DashboardHome

View file

@ -1,4 +1,3 @@
import "@styles/globals.css";
export default function RootLayout({ children, params }: { children: React.ReactNode , params:any}) {

View file

@ -1,25 +0,0 @@
"use client";
import { motion } from "framer-motion";
export default function Template({ children }: { children: React.ReactNode }) {
const variants = {
hidden: { opacity: 0, x: 0, y: 0 },
enter: { opacity: 1, x: 0, y: 0 },
exit: { opacity: 0, x: 0, y: 0 },
};
return (
<div>
<motion.main
variants={variants} // Pass the variant object into Framer Motion
initial="hidden" // Set the initial state to variants.hidden
animate="enter" // Animated state to variants.enter
exit="exit" // Exit state (used later) to variants.exit
transition={{ type: "linear" }} // Set the transition to linear
className=""
>
{children}
</motion.main>
</div>
);
}

View file

@ -0,0 +1,62 @@
'use client';
import { getAPIUrl } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import React, { createContext, useContext, useEffect, useReducer } from 'react'
import useSWR, { mutate } from 'swr';
export const CourseContext = createContext(null) as any;
export const CourseDispatchContext = createContext(null) as any;
export function CourseProvider({ children, courseuuid }: { children: React.ReactNode, courseuuid: string }) {
const { data: courseStructureData } = useSWR(`${getAPIUrl()}courses/${courseuuid}/meta`, swrFetcher);
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer,
{
courseStructure: courseStructureData ? courseStructureData : {},
courseOrder: {},
isSaved: true
}
);
// When courseStructureData is loaded, update the state
useEffect(() => {
if (courseStructureData) {
dispatchCourseStructure({ type: 'setCourseStructure', payload: courseStructureData });
}
}, [courseStructureData]);
if (!courseStructureData) return <div>Loading...</div>
return (
<CourseContext.Provider value={courseStructure}>
<CourseDispatchContext.Provider value={dispatchCourseStructure}>
{children}
</CourseDispatchContext.Provider>
</CourseContext.Provider>
)
}
export function useCourse() {
return useContext(CourseContext);
}
export function useCourseDispatch() {
return useContext(CourseDispatchContext);
}
function courseReducer(state: any, action: any) {
switch (action.type) {
case 'setCourseStructure':
return { ...state, courseStructure: action.payload };
case 'setCourseOrder':
return { ...state, courseOrder: action.payload };
case 'setIsSaved':
return { ...state, isSaved: true };
case 'setIsNotSaved':
return { ...state, isSaved: false };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}

View file

@ -0,0 +1,93 @@
import { useCourse } from '@components/Dashboard/CourseContext';
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity';
import Modal from '@components/StyledElements/Modal/Modal';
import { getAPIUrl } from '@services/config/config';
import { createActivity, createExternalVideoActivity, createFileActivity } from '@services/courses/activities';
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
import { revalidateTags } from '@services/utils/ts/requests';
import { Sparkles } from 'lucide-react'
import { useRouter } from 'next/navigation';
import React, { use, useEffect } from 'react'
import { mutate } from 'swr';
type NewActivityButtonProps = {
chapterId: string,
orgslug: string
}
function NewActivityButton(props: NewActivityButtonProps) {
const [newActivityModal, setNewActivityModal] = React.useState(false);
const router = useRouter();
const course = useCourse() as any;
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true);
};
const closeNewActivityModal = async () => {
setNewActivityModal(false);
};
// Submit new activity
const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfoWithoutCredentials(props.orgslug, { revalidate: 1800 });
await createActivity(activity, props.chapterId, org.org_id);
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
setNewActivityModal(false);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
};
// Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
await createFileActivity(file, type, activity, chapterId);
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
setNewActivityModal(false);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
};
// Submit YouTube Video Upload
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
await createExternalVideoActivity(external_video_data, activity, props.chapterId);
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
setNewActivityModal(false);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
};
useEffect(() => { }
, [course])
return (
<div>
<Modal
isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={props.chapterId}
course={course}
></NewActivityModal>}
dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
<div onClick={() => {
openNewActivityModal(props.chapterId)
}} className="flex space-x-2 items-center py-2 my-3 rounded-md justify-center text-white bg-black hover:cursor-pointer">
<Sparkles className="" size={17} />
<div className="text-sm mx-auto my-auto items-center font-bold">Add Activity + </div>
</div>
</div>
)
}
export default NewActivityButton

View file

@ -0,0 +1,96 @@
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import { deleteActivity } from '@services/courses/activities'
import { revalidateTags } from '@services/utils/ts/requests'
import { Eye, File, MoreVertical, Pencil, Save, Sparkles, Video, X } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import React from 'react'
import { Draggable } from 'react-beautiful-dnd'
import { mutate } from 'swr'
type ActivitiyElementProps = {
orgslug: string,
activity: any,
activityIndex: any,
course_uuid: string
}
function ActivityElement(props: ActivitiyElementProps) {
const router = useRouter();
async function deleteActivityUI() {
await deleteActivity(props.activity.id);
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
}
return (
<Draggable key={props.activity.activity_uuid} draggableId={props.activity.activity_uuid} index={props.activityIndex}>
{(provided, snapshot) => (
<div
className="flex flex-row py-2 my-2 w-full rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear"
key={props.activity.id}
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{/* Activity Type Icon */}
<div className="px-3 text-gray-300 space-x-1 w-28" >
{props.activity.activity_type === "video" &&
<>
<div className="flex space-x-2 items-center">
<Video size={16} />
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">Video</div>
</div>
</>}
</div>
{/* Centered Activity Name */}
<div className="grow items-center space-x-2 flex mx-auto justify-center">
{(<p className="first-letter:uppercase"> {props.activity.name} </p>)}
<Pencil size={12} className="text-neutral-400 hover:cursor-pointer" />
</div>
{/* Edit and View Button */}
<div className="flex flex-row space-x-2">
{props.activity.activity_type === "TYPE_DYNAMIC" && <>
<Link
href={''}
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
rel="noopener noreferrer">
<div className="text-sky-100 font-bold text-xs" >Edit </div>
</Link>
</>}
<Link
href={''}
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
rel="noopener noreferrer">
<Eye strokeWidth={2} size={15} className="text-gray-600" />
</Link>
</div>
{/* Delete Button */}
<div className="flex flex-row pr-3 space-x-1 items-center">
<MoreVertical size={15} className="text-gray-300" />
<ConfirmationModal
confirmationMessage="Are you sure you want to delete this activity ?"
confirmationButtonText="Delete Activity"
dialogTitle={"Delete " + props.activity.name + " ?"}
dialogTrigger={
<div
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
rel="noopener noreferrer">
<X size={15} className="text-rose-200 font-bold" />
</div>}
functionToExecute={() => deleteActivityUI()}
status='warning'
></ConfirmationModal></div>
</div>
)}
</Draggable>
)
}
export default ActivityElement

View file

@ -0,0 +1,104 @@
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
import { Activity, Hexagon, MoreHorizontal, MoreVertical, Pencil, Save, Sparkles, X } from 'lucide-react';
import React from 'react'
import ActivitiyElement from './ActivityElement';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import ActivityElement from './ActivityElement';
import NewActivity from '../Buttons/NewActivityButton';
import NewActivityButton from '../Buttons/NewActivityButton';
import { deleteChapter } from '@services/courses/chapters';
import { revalidateTags } from '@services/utils/ts/requests';
import { useRouter } from 'next/navigation';
import { getAPIUrl } from '@services/config/config';
import { mutate } from 'swr';
type ChapterElementProps = {
chapter: any,
chapterIndex: number,
orgslug: string
course_uuid: string
}
function ChapterElement(props: ChapterElementProps) {
const activities = props.chapter.activities || [];
const router = useRouter();
const deleteChapterUI = async () => {
await deleteChapter(props.chapter.id);
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
};
return (
<Draggable
key={props.chapter.chapter_uuid}
draggableId={props.chapter.chapter_uuid}
index={props.chapterIndex}
>
{(provided, snapshot) => (
<div
className="max-w-screen-2xl mx-auto bg-white rounded-xl shadow-sm px-6 pt-6"
key={props.chapter.chapter_uuid}
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
<div className="flex font-bold text-md items-center space-x-2 pb-3" >
<div className="flex grow text-lg space-x-3 items-center rounded-md ">
<div className="bg-neutral-100 rounded-md p-2">
<Hexagon strokeWidth={3} size={16} className="text-neutral-600 " />
</div>
<div className="flex space-x-2 items-center">
<p className="text-neutral-700 first-letter:uppercase">{props.chapter.name} </p>
<Pencil size={15} className="text-neutral-600 hover:cursor-pointer" />
</div>
</div>
<MoreVertical size={15} className="text-gray-300" />
<ConfirmationModal
confirmationButtonText="Delete Chapter"
confirmationMessage="Are you sure you want to delete this chapter?"
dialogTitle={"Delete " + props.chapter.name + " ?"}
dialogTrigger={
<div
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm"
rel="noopener noreferrer">
<X size={15} className="text-rose-200 font-bold" />
<p>Delete Chapter</p>
</div>}
functionToExecute={() => deleteChapterUI()}
status='warning'
></ConfirmationModal>
</div>
<Droppable key={props.chapter.chapter_uuid} droppableId={props.chapter.chapter_uuid} type="activity">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
<div className="flex flex-col">
{activities.map((activity: any, index: any) => {
return (
<div key={index} className="flex items-center ">
<ActivityElement
orgslug={props.orgslug}
course_uuid={props.course_uuid}
activityIndex={index}
activity={activity} />
</div>
)
})}
{provided.placeholder}
</div>
</div>
)}
</Droppable>
<NewActivityButton orgslug={props.orgslug} chapterId={props.chapter.id} />
<div className='h-6'>
<div className='flex items-center'><MoreHorizontal size={19} className="text-gray-300 mx-auto" /></div>
</div>
</div>
)}
</Draggable>
)
}
export default ChapterElement

View file

@ -0,0 +1,111 @@
'use client';
import { getAPIUrl } from '@services/config/config';
import { revalidateTags, swrFetcher } from '@services/utils/ts/requests';
import React, { useContext, useEffect, useState } from 'react'
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import useSWR, { mutate } from 'swr';
import ChapterElement from './DraggableElements/ChapterElement';
import PageLoading from '@components/Objects/Loaders/PageLoading';
import { updateCourseOrderStructure } from '@services/courses/chapters';
import { useRouter } from 'next/navigation';
import { CourseStructureContext } from 'app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page';
import { useCourse, useCourseDispatch } from '@components/Dashboard/CourseContext';
type EditCourseStructureProps = {
orgslug: string,
course_uuid?: string,
}
export type OrderPayload = {
chapter_order_by_ids: [
{
chapter_id: string,
activities_order_by_ids: [
{
activity_id: string
}
]
}
],
} | undefined
const EditCourseStructure = (props: EditCourseStructureProps) => {
const router = useRouter();
// Check window availability
const [winReady, setwinReady] = useState(false);
const dispatchCourse = useCourseDispatch() as any;
const [order, setOrder] = useState<OrderPayload>();
const course = useCourse() as any;
const course_structure = course ? course.courseStructure : {};
const course_uuid = course ? course.courseStructure.course_uuid : '';
const updateStructure = (result: any) => {
const { destination, source, draggableId, type } = result;
if (!destination) return;
if (destination.droppableId === source.droppableId && destination.index === source.index) return;
if (type === 'chapter') {
const newChapterOrder = Array.from(course_structure.chapters);
newChapterOrder.splice(source.index, 1);
newChapterOrder.splice(destination.index, 0, course_structure.chapters[source.index]);
dispatchCourse({ type: 'setCourseStructure', payload: { ...course_structure, chapters: newChapterOrder } })
dispatchCourse({ type: 'setIsNotSaved' })
}
if (type === 'activity') {
const newChapterOrder = Array.from(course_structure.chapters);
const sourceChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === source.droppableId) as any;
const destinationChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === destination.droppableId) ? newChapterOrder.find((chapter: any) => chapter.chapter_uuid === destination.droppableId) : sourceChapter;
const activity = sourceChapter.activities.find((activity: any) => activity.activity_uuid === draggableId);
sourceChapter.activities.splice(source.index, 1);
destinationChapter.activities.splice(destination.index, 0, activity);
dispatchCourse({ type: 'setCourseStructure', payload: { ...course_structure, chapters: newChapterOrder } })
dispatchCourse({ type: 'setIsNotSaved' })
}
}
useEffect(() => {
setwinReady(true);
}, [props.course_uuid, course_structure, course]);
if (!course) return <PageLoading></PageLoading>
return (
<div className='flex flex-col'>
{winReady ?
<DragDropContext onDragEnd={updateStructure}>
<Droppable type='chapter' droppableId='chapters'>
{(provided) => (
<div
className='space-y-4'
{...provided.droppableProps}
ref={provided.innerRef}>
{course_structure.chapters && course_structure.chapters.map((chapter: any, index: any) => {
return (
<ChapterElement
key={chapter.chapter_uuid}
chapterIndex={index}
orgslug={props.orgslug}
course_uuid={course_uuid}
chapter={chapter} />
)
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
: <></>}
</div>
)
}
export default EditCourseStructure

View file

@ -0,0 +1,30 @@
import { useCourse } from '@components/Dashboard/CourseContext'
import { Book, ChevronRight, User } from 'lucide-react'
import Link from 'next/link'
import React, { use, useEffect } from 'react'
type BreadCrumbsProps = {
type: 'courses' | 'users'
last_breadcrumb?: string
}
function BreadCrumbs(props: BreadCrumbsProps) {
const course = useCourse() as any;
return (
<div>
<div className='h-7'></div>
<div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
<div className='flex items-center space-x-1'>
{props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''}
{props.type == 'users' ? <div> <User size={14}></User><Link href='/dash/users'>Users</Link></div> : ''}
<div className='flex items-center space-x-1 first-letter:uppercase'>
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
<div className='first-letter:uppercase'> {props.last_breadcrumb}</div>
</div></div></div>
</div>
)
}
export default BreadCrumbs

View file

@ -0,0 +1,31 @@
import { useCourse } from "@components/Dashboard/CourseContext";
import { useEffect } from "react";
import BreadCrumbs from "./BreadCrumbs";
import SaveState from "./SaveState";
import { CourseOverviewParams } from "app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page";
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
const course = useCourse() as any;
useEffect(() => { }
, [course])
return (
<>
<BreadCrumbs type='courses' last_breadcrumb={course.courseStructure.name} ></BreadCrumbs>
<div className='flex'>
<div className='flex py-5 grow items-center'>
<div className="image rounded-lg shadow-md bg-gray-900 w-28 h-14"></div>
<div className="flex flex-col course_metadata justify-center pl-5">
<div className='text-gray-400 font-semibold text-sm'>Course</div>
<div className='text-black font-bold text-xl -mt-1 first-letter:uppercase'>{course.courseStructure.name}</div>
</div>
</div>
<div className='flex items-center'>
<SaveState orgslug={params.orgslug} />
</div>
</div>
</>
)
}

View file

@ -0,0 +1,22 @@
import { Book } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
function LeftMenu() {
return (
<div
style={{ background: "linear-gradient(0deg, rgba(0, 0, 0, 0.20) 0%, rgba(0, 0, 0, 0.20) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0.00) 100%), #2E2D2D" }}
className='flex flex-col w-20 justifiy-center bg-black h-screen justify-center text-white'>
<div className='flex items-center mx-auto'>
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><Book size={18}/></Link>
</div>
</div>
)
}
export default LeftMenu

View file

@ -0,0 +1,100 @@
'use client';
import { getAPIUrl } from '@services/config/config';
import { updateCourseOrderStructure } from '@services/courses/chapters';
import { revalidateTags } from '@services/utils/ts/requests';
import { useCourse, useCourseDispatch } from '@components/Dashboard/CourseContext'
import { Check, SaveAllIcon, Timer } from 'lucide-react'
import { useRouter } from 'next/navigation';
import React, { useEffect } from 'react'
import { mutate } from 'swr';
function SaveState(props: { orgslug: string }) {
const course = useCourse() as any;
const router = useRouter();
const saved = course ? course.isSaved : true;
const dispatchCourse = useCourseDispatch() as any;
const course_structure = course.courseStructure;
const saveCourseState = async () => {
// Course structure & order
if (saved) return;
await changeOrderBackend();
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
dispatchCourse({ type: 'setIsSaved' })
}
//
// Course Order
const changeOrderBackend = async () => {
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
await updateCourseOrderStructure(course.courseStructure.course_uuid, course.courseOrder);
await revalidateTags(['courses'], props.orgslug)
router.refresh();
dispatchCourse({ type: 'setIsSaved' })
}
const handleCourseOrder = (course_structure: any) => {
const chapters = course_structure.chapters;
const chapter_order_by_ids = chapters.map((chapter: any) => {
return {
chapter_id: chapter.id,
activities_order_by_ids: chapter.activities.map((activity: any) => {
return {
activity_id: activity.id
}
})
}
})
dispatchCourse({ type: 'setCourseOrder', payload: { chapter_order_by_ids: chapter_order_by_ids } })
dispatchCourse({ type: 'setIsNotSaved' })
}
const initOrderPayload = () => {
if (course_structure && course_structure.chapters) {
handleCourseOrder(course_structure);
dispatchCourse({ type: 'setIsSaved' })
}
}
const changeOrderPayload = () => {
if (course_structure && course_structure.chapters) {
handleCourseOrder(course_structure);
dispatchCourse({ type: 'setIsNotSaved' })
}
}
useEffect(() => {
if (course_structure?.chapters) {
initOrderPayload();
}
if (course_structure?.chapters && !saved) {
changeOrderPayload();
}
}, [course_structure]); // This effect depends on the `course_structure` variable
return (
<div className='flex space-x-4'>
{saved ? <></> : <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 ` + (saved ? 'bg-gray-600 text-white' : 'bg-black text-white border hover:bg-gray-900 ')
} onClick={saveCourseState}>
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />}
{saved ? <div className=''>Saved</div> : <div className=''>Save</div>}
</div>
</div>
)
}
export default SaveState

View file

@ -1,21 +1,6 @@
// This file sets a custom webpack configuration to use your Next.js app
// with Sentry.
// https://nextjs.org/docs/api-reference/next.config.js/introduction
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
const { withSentryConfig } = require('@sentry/nextjs');
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
compiler: {
styledComponents: true,
},
};
reactStrictMode: false,
}
module.exports = nextConfig;
module.exports = withSentryConfig(
module.exports,
{ silent: true },
{ hideSourcemaps: true },
);
module.exports = nextConfig

View file

@ -28,7 +28,7 @@
"framer-motion": "^10.16.1",
"lowlight": "^3.0.0",
"lucide-react": "^0.268.0",
"next": "^14.0.3",
"next": "^14.0.4",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
@ -2266,9 +2266,9 @@
}
},
"node_modules/@next/env": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz",
"integrity": "sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA=="
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
"integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.5.4",
@ -2300,9 +2300,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz",
"integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz",
"integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==",
"cpu": [
"arm64"
],
@ -2315,9 +2315,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz",
"integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz",
"integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==",
"cpu": [
"x64"
],
@ -2330,9 +2330,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz",
"integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz",
"integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==",
"cpu": [
"arm64"
],
@ -2345,9 +2345,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz",
"integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz",
"integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==",
"cpu": [
"arm64"
],
@ -2360,9 +2360,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz",
"integrity": "sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz",
"integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==",
"cpu": [
"x64"
],
@ -2375,9 +2375,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz",
"integrity": "sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz",
"integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==",
"cpu": [
"x64"
],
@ -2390,9 +2390,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz",
"integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz",
"integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==",
"cpu": [
"arm64"
],
@ -2405,9 +2405,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz",
"integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz",
"integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==",
"cpu": [
"ia32"
],
@ -2420,9 +2420,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz",
"integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz",
"integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==",
"cpu": [
"x64"
],
@ -7117,14 +7117,15 @@
"dev": true
},
"node_modules/next": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz",
"integrity": "sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==",
"version": "14.0.4",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz",
"integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==",
"dependencies": {
"@next/env": "14.0.3",
"@next/env": "14.0.4",
"@swc/helpers": "0.5.2",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1",
"watchpack": "2.4.0"
@ -7136,15 +7137,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.0.3",
"@next/swc-darwin-x64": "14.0.3",
"@next/swc-linux-arm64-gnu": "14.0.3",
"@next/swc-linux-arm64-musl": "14.0.3",
"@next/swc-linux-x64-gnu": "14.0.3",
"@next/swc-linux-x64-musl": "14.0.3",
"@next/swc-win32-arm64-msvc": "14.0.3",
"@next/swc-win32-ia32-msvc": "14.0.3",
"@next/swc-win32-x64-msvc": "14.0.3"
"@next/swc-darwin-arm64": "14.0.4",
"@next/swc-darwin-x64": "14.0.4",
"@next/swc-linux-arm64-gnu": "14.0.4",
"@next/swc-linux-arm64-musl": "14.0.4",
"@next/swc-linux-x64-gnu": "14.0.4",
"@next/swc-linux-x64-musl": "14.0.4",
"@next/swc-win32-arm64-msvc": "14.0.4",
"@next/swc-win32-ia32-msvc": "14.0.4",
"@next/swc-win32-x64-msvc": "14.0.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",

View file

@ -29,7 +29,7 @@
"framer-motion": "^10.16.1",
"lowlight": "^3.0.0",
"lucide-react": "^0.268.0",
"next": "^14.0.3",
"next": "14.0.4",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",

View file

@ -40,7 +40,7 @@ export async function createExternalVideoActivity(data: any, activity: any, chap
data.chapter_id = chapter_id;
data.activity_id = activity.id;
const result = await fetch(`${getAPIUrl()}activities/external_video?coursechapter_id=${chapter_id}`, RequestBody("POST", data, null));
const result = await fetch(`${getAPIUrl()}activities/external_video`, RequestBody("POST", data, null));
const res = await result.json();
return res;
}

View file

@ -1,3 +1,4 @@
import { OrderPayload } from "@components/Dashboard/EditCourseStructure/EditCourseStructure";
import { getAPIUrl } from "@services/config/config";
import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests";
@ -25,6 +26,12 @@ export async function updateChapter(coursechapter_id: any, data: any) {
return res;
}
export async function updateCourseOrderStructure(course_uuid: any, data: OrderPayload) {
const result: any = await fetch(`${getAPIUrl()}chapters/course/${course_uuid}/order`, RequestBody("PUT", data, null));
const res = await errorHandling(result);
return res;
}
export async function createChapter(data: any) {
const result: any = await fetch(`${getAPIUrl()}chapters/`, RequestBody("POST", data, null));
const res = await errorHandling(result);

View file

@ -5,11 +5,10 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
@ -29,7 +28,7 @@
"@editor/*": ["components/Objects/Editor/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx","**/**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
}