mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge branch 'learnhouse:dev' into Ordered-List
This commit is contained in:
commit
6b6b3e65f9
27 changed files with 4057 additions and 8816 deletions
|
|
@ -14,4 +14,4 @@ RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt
|
||||||
COPY ./ /usr/learnhouse
|
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" ]
|
||||||
|
|
|
||||||
2
app.py
2
app.py
|
|
@ -8,8 +8,6 @@ from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi_jwt_auth.exceptions import AuthJWTException
|
from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
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
|
# from src.services.mocks.initial import create_initial_data
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const CollectionAdminEditsArea = (props: any) => {
|
||||||
|
|
||||||
const deleteCollectionUI = async (collectionId: number) => {
|
const deleteCollectionUI = async (collectionId: number) => {
|
||||||
await deleteCollection(collectionId);
|
await deleteCollection(collectionId);
|
||||||
revalidateTags(["collections"], props.orgslug);
|
await revalidateTags(["collections"], props.orgslug);
|
||||||
// reload the page
|
// reload the page
|
||||||
router.refresh();
|
router.refresh();
|
||||||
router.push(getUriWithOrg(props.orgslug, "/collections"));
|
router.push(getUriWithOrg(props.orgslug, "/collections"));
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,10 @@ function NewCollection(params: any) {
|
||||||
org_id: org.org_id,
|
org_id: org.org_id,
|
||||||
};
|
};
|
||||||
await createCollection(collection);
|
await createCollection(collection);
|
||||||
revalidateTags(["collections"], orgslug);
|
await revalidateTags(["collections"], orgslug);
|
||||||
|
router.refresh();
|
||||||
router.prefetch(getUriWithOrg(orgslug, "/collections"));
|
router.prefetch(getUriWithOrg(orgslug, "/collections"));
|
||||||
router.push(getUriWithOrg(orgslug, "/collections"));
|
router.push(getUriWithOrg(orgslug, "/collections"));
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const CollectionsPage = async (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||||
const org_id = org.org_id;
|
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 (
|
return (
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
|
|
@ -54,7 +54,7 @@ const CollectionsPage = async (params: any) => {
|
||||||
<div className="home_collections flex flex-wrap">
|
<div className="home_collections flex flex-wrap">
|
||||||
{collections.map((collection: any) => (
|
{collections.map((collection: any) => (
|
||||||
<div className="flex flex-col py-3 px-3" key={collection.collection_id}>
|
<div className="flex flex-col py-3 px-3" key={collection.collection_id}>
|
||||||
<CollectionAdminEditsArea org_id={org_id} collection_id={collection.collection_id} collection={collection} />
|
<CollectionAdminEditsArea orgslug={orgslug} org_id={org_id} collection_id={collection.collection_id} collection={collection} />
|
||||||
<Link href={getUriWithOrg(orgslug, "/collection/" + removeCollectionPrefix(collection.collection_id))}>
|
<Link href={getUriWithOrg(orgslug, "/collection/" + removeCollectionPrefix(collection.collection_id))}>
|
||||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[180px] bg-cover flex flex-col items-center justify-center bg-indigo-600 font-bold text-zinc-50" >
|
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[180px] bg-cover flex flex-col items-center justify-center bg-indigo-600 font-bold text-zinc-50" >
|
||||||
<h1 className="font-bold text-lg py-2 justify-center mb-2">{collection.name}</h1>
|
<h1 className="font-bold text-lg py-2 justify-center mb-2">{collection.name}</h1>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const CourseClient = (props: any) => {
|
||||||
async function startCourseUI() {
|
async function startCourseUI() {
|
||||||
// Create activity
|
// Create activity
|
||||||
await startCourse("course_" + courseid, orgslug);
|
await startCourse("course_" + courseid, orgslug);
|
||||||
revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
// refresh page (FIX for Next.js BUG)
|
// refresh page (FIX for Next.js BUG)
|
||||||
|
|
@ -34,7 +34,7 @@ const CourseClient = (props: any) => {
|
||||||
// Close activity
|
// Close activity
|
||||||
let activity = await removeCourse("course_" + courseid, orgslug);
|
let activity = await removeCourse("course_" + courseid, orgslug);
|
||||||
// Mutate course
|
// Mutate course
|
||||||
revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
// refresh page (FIX for Next.js BUG)
|
// refresh page (FIX for Next.js BUG)
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<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_isloading && <div className='text-sm text-gray-500'>Loading...</div>}
|
||||||
|
{course && <>
|
||||||
|
<div className='flex items-center'><div className='info flex space-x-5 items-center grow'>
|
||||||
|
<div className='flex'>
|
||||||
|
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}`}>
|
||||||
|
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseid, 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.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} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} />
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any }) => {
|
||||||
|
if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) {
|
||||||
|
return <CourseEdition data={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} />
|
||||||
|
}
|
||||||
|
else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) {
|
||||||
|
return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} />
|
||||||
|
}
|
||||||
|
else if (subpage.toString() === 'content' || subpage.toString() === 'general') {
|
||||||
|
return <Loading />
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<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,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 (
|
||||||
|
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
|
||||||
|
<div className="login-form">
|
||||||
|
{error && (
|
||||||
|
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||||
|
<AlertTriangle size={18} />
|
||||||
|
<div className="font-bold text-sm">{error}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FormLayout onSubmit={formik.handleSubmit}>
|
||||||
|
<FormField name="name">
|
||||||
|
<FormLabelAndMessage label='Name' message={formik.errors.name} />
|
||||||
|
<Form.Control asChild>
|
||||||
|
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
|
||||||
|
</Form.Control>
|
||||||
|
</FormField>
|
||||||
|
<FormField name="mini_description">
|
||||||
|
<FormLabelAndMessage label='Mini description' message={formik.errors.mini_description} />
|
||||||
|
<Form.Control asChild>
|
||||||
|
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.mini_description} type="text" required />
|
||||||
|
</Form.Control>
|
||||||
|
</FormField>
|
||||||
|
<FormField name="description">
|
||||||
|
<FormLabelAndMessage label='Description' message={formik.errors.description} />
|
||||||
|
<Form.Control asChild>
|
||||||
|
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
|
||||||
|
</Form.Control>
|
||||||
|
</FormField>
|
||||||
|
<FormField name="learnings">
|
||||||
|
<FormLabelAndMessage label='Learnings (Separated by , )' message={formik.errors.learnings} />
|
||||||
|
<Form.Control asChild>
|
||||||
|
<Textarea placeholder='Science, Design, Architecture' style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
|
||||||
|
</Form.Control>
|
||||||
|
</FormField>
|
||||||
|
</FormLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CourseEdition
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
|
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal';
|
||||||
import { Edit2, Trash } from "lucide-react";
|
|
||||||
import { getBackendUrl, getUriWithOrg } from '@services/config/config';
|
import { getBackendUrl, getUriWithOrg } from '@services/config/config';
|
||||||
import CoursesLogo from "public/svg/courses.svg";
|
import CoursesLogo from "public/svg/courses.svg";
|
||||||
import CollectionsLogo from "public/svg/collections.svg";
|
import CollectionsLogo from "public/svg/collections.svg";
|
||||||
|
|
@ -37,7 +36,7 @@ function Courses(props: CourseProps) {
|
||||||
|
|
||||||
async function deleteCourses(course_id: any) {
|
async function deleteCourses(course_id: any) {
|
||||||
await deleteCourseFromBackend(course_id);
|
await deleteCourseFromBackend(course_id);
|
||||||
revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug);
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
|
|
@ -85,10 +84,7 @@ function Courses(props: CourseProps) {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -102,14 +98,14 @@ const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any,
|
||||||
dialogTitle={'Delete ' + props.course.name + ' ?'}
|
dialogTitle={'Delete ' + props.course.name + ' ?'}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<button className="rounded-md text-sm px-3 font-bold text-red-800 bg-red-200 w-16 flex justify-center items-center" >
|
<button className="rounded-md text-sm px-3 font-bold text-red-800 bg-red-200 w-16 flex justify-center items-center" >
|
||||||
Delete <Trash size={10}></Trash>
|
Delete
|
||||||
</button>}
|
</button>}
|
||||||
functionToExecute={() => props.deleteCourses(props.courseId)}
|
functionToExecute={() => props.deleteCourses(props.courseId)}
|
||||||
status='warning'
|
status='warning'
|
||||||
></ConfirmationModal>
|
></ConfirmationModal>
|
||||||
<Link href={getUriWithOrg(props.orgSlug, "/course/" + removeCoursePrefix(props.courseId) + "/edit")}>
|
<Link href={getUriWithOrg(props.orgSlug, "/course/" + removeCoursePrefix(props.courseId) + "/edit")}>
|
||||||
<button className="rounded-md text-sm px-3 font-bold text-orange-800 bg-orange-200 w-16 flex justify-center items-center">
|
<button className="rounded-md text-sm px-3 font-bold text-orange-800 bg-orange-200 w-16 flex justify-center items-center">
|
||||||
Edit <Edit2 size={10}></Edit2>
|
Edit
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@ import "@styles/globals.css";
|
||||||
import { Menu } from "@components/Objects/Menu/Menu";
|
import { Menu } from "@components/Objects/Menu/Menu";
|
||||||
import AuthProvider from "@components/Security/AuthProvider";
|
import AuthProvider from "@components/Security/AuthProvider";
|
||||||
|
|
||||||
export default async function RootLayout({ children, params }: { children: React.ReactNode , params:any}) {
|
export default async function RootLayout({ children, params }: { children: React.ReactNode , params :any}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Menu orgslug={params.orgslug}></Menu>
|
<Menu orgslug={params?.orgslug}></Menu>
|
||||||
{children}
|
{children}
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const OrgHomePage = async (params: any) => {
|
||||||
|
|
||||||
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null);
|
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null);
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||||
const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token_cookie ? access_token_cookie.value : null);
|
const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['courses'] });
|
||||||
|
|
||||||
|
|
||||||
// function to remove "course_" from the course_id
|
// function to remove "course_" from the course_id
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ function OrganizationClient(props: any) {
|
||||||
let org_id = org.org_id;
|
let org_id = org.org_id;
|
||||||
await uploadOrganizationLogo(org_id, selectedFile);
|
await uploadOrganizationLogo(org_id, selectedFile);
|
||||||
setSelectedFile(null); // Reset the selected file
|
setSelectedFile(null); // Reset the selected file
|
||||||
revalidateTags(['organizations'], org.slug);
|
await revalidateTags(['organizations'], org.slug);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ function OrganizationClient(props: any) {
|
||||||
await updateOrganization(org_id, values);
|
await updateOrganization(org_id, values);
|
||||||
|
|
||||||
// Mutate the org
|
// Mutate the org
|
||||||
revalidateTags(['organizations'], org.slug);
|
await revalidateTags(['organizations'], org.slug);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import useSWR from "swr";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from "@services/config/config";
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||||
import ClientComponentSkeleton from "@components/Utils/ClientComp";
|
import ClientComponentSkeleton from "@components/Utils/ClientComp";
|
||||||
import { HeaderProfileBox } from "@components/Security/HeaderProfileBox";
|
import { HeaderProfileBox } from "@components/Security/HeaderProfileBox";
|
||||||
import { swrFetcher } from "@services/utils/ts/requests";
|
|
||||||
import MenuLinks from "./MenuLinks";
|
import MenuLinks from "./MenuLinks";
|
||||||
import { getOrgLogoMediaDirectory } from "@services/media/media";
|
import { getOrgLogoMediaDirectory } from "@services/media/media";
|
||||||
|
|
||||||
|
|
@ -23,33 +21,35 @@ export const Menu = async (props: any) => {
|
||||||
}}></div>
|
}}></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center px-16 space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50">
|
<div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50">
|
||||||
<div className="logo flex ">
|
<div className="flex items-center space-x-5 w-full max-w-screen-2xl mx-auto px-16">
|
||||||
<Link href={getUriWithOrg(orgslug, "/")}>
|
<div className="logo flex ">
|
||||||
<div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center" >
|
<Link href={getUriWithOrg(orgslug, "/")}>
|
||||||
{org?.logo ? (
|
<div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center" >
|
||||||
<img
|
{org?.logo ? (
|
||||||
src={`${getOrgLogoMediaDirectory(org.org_id, org?.logo)}`}
|
<img
|
||||||
alt="Learnhouse"
|
src={`${getOrgLogoMediaDirectory(org.org_id, org?.logo)}`}
|
||||||
style={{ width: "auto", height: "100%" }}
|
alt="Learnhouse"
|
||||||
className="rounded-md"
|
style={{ width: "auto", height: "100%" }}
|
||||||
/>
|
className="rounded-md"
|
||||||
) : (
|
/>
|
||||||
<LearnHouseLogo></LearnHouseLogo>
|
) : (
|
||||||
)}
|
<LearnHouseLogo></LearnHouseLogo>
|
||||||
</div>
|
)}
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
<div className="links flex grow">
|
</div>
|
||||||
<ClientComponentSkeleton>
|
<div className="links flex grow">
|
||||||
<MenuLinks orgslug={orgslug} />
|
<ClientComponentSkeleton>
|
||||||
</ClientComponentSkeleton>
|
<MenuLinks orgslug={orgslug} />
|
||||||
|
</ClientComponentSkeleton>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="profile flex">
|
<div className="profile flex">
|
||||||
<ClientComponentSkeleton>
|
<ClientComponentSkeleton>
|
||||||
<HeaderProfileBox />
|
<HeaderProfileBox />
|
||||||
</ClientComponentSkeleton>
|
</ClientComponentSkeleton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,13 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
let status = await createNewCourse(orgId, { name, description }, thumbnail);
|
let status = await createNewCourse(orgId, { name, description }, thumbnail);
|
||||||
revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug);
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
||||||
if (status.org_id == orgId) {
|
if (status.org_id == orgId) {
|
||||||
closeModal();
|
closeModal();
|
||||||
router.refresh();
|
router.refresh();
|
||||||
revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug);
|
||||||
|
|
||||||
// refresh page (FIX for Next.js BUG)
|
// refresh page (FIX for Next.js BUG)
|
||||||
// window.location.reload();
|
// window.location.reload();
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function Chapter(props: any) {
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
// isDragging={snapshot.isDragging}
|
// isDragging={snapshot.isDragging}
|
||||||
className=""
|
className="max-w-screen-2xl mx-auto"
|
||||||
key={props.info.list.chapter.id}
|
key={props.info.list.chapter.id}
|
||||||
>
|
>
|
||||||
<h3 className="flex space-x-2 pt-3 font-bold text-md items-center">
|
<h3 className="flex space-x-2 pt-3 font-bold text-md items-center">
|
||||||
|
|
@ -64,7 +64,6 @@ const ChapterWrapper = styled.div`
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background-color: #ffffff9d;
|
background-color: #ffffff9d;
|
||||||
width: 900px;
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function TrailCourseElement(props: TrailCourseElementProps) {
|
||||||
// Close activity
|
// Close activity
|
||||||
let activity = await removeCourse(course_id, props.orgslug);
|
let activity = await removeCourse(course_id, props.orgslug);
|
||||||
// Mutate course
|
// Mutate course
|
||||||
revalidateTags(['courses'], props.orgslug);
|
await revalidateTags(['courses'], props.orgslug);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
// Mutate
|
// Mutate
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ export const HeaderProfileBox = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccountArea = styled.div`
|
const AccountArea = styled.div`
|
||||||
padding-right: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|
||||||
|
|
|
||||||
11772
front/package-lock.json
generated
11772
front/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -26,8 +26,8 @@
|
||||||
"avvvatars-react": "^0.4.2",
|
"avvvatars-react": "^0.4.2",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"framer-motion": "^7.3.6",
|
"framer-motion": "^7.3.6",
|
||||||
"lucide-react": "^0.248.0",
|
"lucide-react": "^0.268.0",
|
||||||
"next": "^13.4.8",
|
"next": "^13.4.19",
|
||||||
"re-resizable": "^6.9.9",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ export async function getOrgCollections() {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrgCollectionsWithAuthHeader(org_id: string, access_token: string) {
|
export async function getOrgCollectionsWithAuthHeader(org_id: string, access_token: string, next: any) {
|
||||||
const result: any = await fetch(`${getAPIUrl()}collections/org_id/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, { revalidate: 3 }, access_token));
|
const result: any = await fetch(`${getAPIUrl()}collections/org_id/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, next, access_token));
|
||||||
const res = await errorHandling(result);
|
const res = await errorHandling(result);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ export async function getCourseMetadataWithAuthHeader(course_id: any, next: any,
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateCourse(course_id: any, data: any) {
|
||||||
|
const result: any = await fetch(`${getAPIUrl()}courses/course_${course_id}`, RequestBody("PUT", data, null));
|
||||||
|
const res = await errorHandling(result);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCourse(course_id: string, next: any) {
|
export async function getCourse(course_id: string, next: any) {
|
||||||
const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next));
|
const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next));
|
||||||
const res = await errorHandling(result);
|
const res = await errorHandling(result);
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ export const errorHandling = (res: any) => {
|
||||||
return res.json();
|
return res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const revalidateTags = (tags: string[], orgslug: string) => {
|
export const revalidateTags = async (tags: string[], orgslug: string) => {
|
||||||
const url = getUriWithOrg(orgslug, "");
|
const url = getUriWithOrg(orgslug, "");
|
||||||
tags.forEach((tag) => {
|
tags.forEach((tag) => {
|
||||||
fetch(`${url}/api/revalidate?tag=${tag}`);
|
fetch(`${url}/api/revalidate?tag=${tag}`);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
fastapi==0.92.0
|
fastapi==0.101.1
|
||||||
pydantic>=1.8.0,<2.0.0
|
pydantic>=1.8.0,<2.0.0
|
||||||
uvicorn==0.20.0
|
uvicorn==0.23.2
|
||||||
pymongo==4.3.3
|
pymongo==4.3.3
|
||||||
motor==3.1.1
|
motor==3.1.1
|
||||||
python-multipart
|
python-multipart
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pprint import pprint
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from src.security.security import security_hash_password
|
from src.security.security import security_hash_password
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue