mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
fix: thumbnails
This commit is contained in:
parent
c39d9d5340
commit
3413e6ca73
33 changed files with 161 additions and 740 deletions
|
|
@ -49,7 +49,7 @@ async def get_course(
|
||||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||||
)
|
)
|
||||||
authors = db_session.exec(authors_statement).all()
|
authors = db_session.exec(authors_statement).all()
|
||||||
|
|
||||||
# convert from User to UserRead
|
# convert from User to UserRead
|
||||||
authors = [UserRead.from_orm(author) for author in authors]
|
authors = [UserRead.from_orm(author) for author in authors]
|
||||||
|
|
||||||
|
|
@ -124,6 +124,11 @@ async def create_course(
|
||||||
|
|
||||||
# Complete course object
|
# Complete course object
|
||||||
course.org_id = course.org_id
|
course.org_id = course.org_id
|
||||||
|
|
||||||
|
# Get org uuid
|
||||||
|
org_statement = select(Organization).where(Organization.id == org_id)
|
||||||
|
org = db_session.exec(org_statement).first()
|
||||||
|
|
||||||
course.course_uuid = str(f"course_{uuid4()}")
|
course.course_uuid = str(f"course_{uuid4()}")
|
||||||
course.creation_date = str(datetime.now())
|
course.creation_date = str(datetime.now())
|
||||||
course.update_date = str(datetime.now())
|
course.update_date = str(datetime.now())
|
||||||
|
|
@ -132,9 +137,9 @@ async def create_course(
|
||||||
if thumbnail_file and thumbnail_file.filename:
|
if thumbnail_file and thumbnail_file.filename:
|
||||||
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||||
await upload_thumbnail(
|
await upload_thumbnail(
|
||||||
thumbnail_file, name_in_disk, org_id, course.course_uuid
|
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
|
||||||
)
|
)
|
||||||
course_object.thumbnail_image = name_in_disk
|
course.thumbnail_image = name_in_disk
|
||||||
|
|
||||||
# Insert course
|
# Insert course
|
||||||
db_session.add(course)
|
db_session.add(course)
|
||||||
|
|
@ -192,11 +197,15 @@ async def update_course_thumbnail(
|
||||||
# RBAC check
|
# RBAC check
|
||||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||||
|
|
||||||
|
# Get org uuid
|
||||||
|
org_statement = select(Organization).where(Organization.id == course.org_id)
|
||||||
|
org = db_session.exec(org_statement).first()
|
||||||
|
|
||||||
# Upload thumbnail
|
# Upload thumbnail
|
||||||
if thumbnail_file and thumbnail_file.filename:
|
if thumbnail_file and thumbnail_file.filename:
|
||||||
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||||
await upload_thumbnail(
|
await upload_thumbnail(
|
||||||
thumbnail_file, name_in_disk, course.org_id, course.course_uuid
|
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update course
|
# Update course
|
||||||
|
|
@ -223,8 +232,6 @@ async def update_course_thumbnail(
|
||||||
)
|
)
|
||||||
authors = db_session.exec(authors_statement).all()
|
authors = db_session.exec(authors_statement).all()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# convert from User to UserRead
|
# convert from User to UserRead
|
||||||
authors = [UserRead.from_orm(author) for author in authors]
|
authors = [UserRead.from_orm(author) for author in authors]
|
||||||
|
|
||||||
|
|
@ -331,7 +338,7 @@ async def get_courses_orgslug(
|
||||||
|
|
||||||
courses = db_session.exec(statement)
|
courses = db_session.exec(statement)
|
||||||
|
|
||||||
courses = [CourseRead(**course.dict(),authors=[]) for course in courses]
|
courses = [CourseRead(**course.dict(), authors=[]) for course in courses]
|
||||||
|
|
||||||
# for every course, get the authors
|
# for every course, get the authors
|
||||||
for course in courses:
|
for course in courses:
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ from uuid import uuid4
|
||||||
from src.services.utils.upload_content import upload_content
|
from src.services.utils.upload_content import upload_content
|
||||||
|
|
||||||
|
|
||||||
async def upload_org_logo(logo_file, org_id):
|
async def upload_org_logo(logo_file, org_uuid):
|
||||||
contents = logo_file.file.read()
|
contents = logo_file.file.read()
|
||||||
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
|
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
|
||||||
|
|
||||||
await upload_content(
|
await upload_content(
|
||||||
"logos",
|
"logos",
|
||||||
org_id,
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
name_in_disk,
|
name_in_disk,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ async def update_org_logo(
|
||||||
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
|
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
|
||||||
|
|
||||||
# Upload logo
|
# Upload logo
|
||||||
name_in_disk = await upload_org_logo(logo_file, org_id)
|
name_in_disk = await upload_org_logo(logo_file, org.org_uuid)
|
||||||
|
|
||||||
# Update org
|
# Update org
|
||||||
org.logo_image = name_in_disk
|
org.logo_image = name_in_disk
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from config.config import get_learnhouse_config
|
||||||
|
|
||||||
|
|
||||||
async def upload_content(
|
async def upload_content(
|
||||||
directory: str, org_id: str, file_binary: bytes, file_and_format: str
|
directory: str, org_uuid: str, file_binary: bytes, file_and_format: str
|
||||||
):
|
):
|
||||||
# Get Learnhouse Config
|
# Get Learnhouse Config
|
||||||
learnhouse_config = get_learnhouse_config()
|
learnhouse_config = get_learnhouse_config()
|
||||||
|
|
@ -16,12 +16,12 @@ async def upload_content(
|
||||||
|
|
||||||
if content_delivery == "filesystem":
|
if content_delivery == "filesystem":
|
||||||
# create folder for activity
|
# create folder for activity
|
||||||
if not os.path.exists(f"content/{org_id}/{directory}"):
|
if not os.path.exists(f"content/{org_uuid}/{directory}"):
|
||||||
# create folder for activity
|
# create folder for activity
|
||||||
os.makedirs(f"content/{org_id}/{directory}")
|
os.makedirs(f"content/{org_uuid}/{directory}")
|
||||||
# upload file to server
|
# upload file to server
|
||||||
with open(
|
with open(
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
"wb",
|
"wb",
|
||||||
) as f:
|
) as f:
|
||||||
f.write(file_binary)
|
f.write(file_binary)
|
||||||
|
|
@ -37,13 +37,13 @@ async def upload_content(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create folder for activity
|
# Create folder for activity
|
||||||
if not os.path.exists(f"content/{org_id}/{directory}"):
|
if not os.path.exists(f"content/{org_uuid}/{directory}"):
|
||||||
# create folder for activity
|
# create folder for activity
|
||||||
os.makedirs(f"content/{org_id}/{directory}")
|
os.makedirs(f"content/{org_uuid}/{directory}")
|
||||||
|
|
||||||
# Upload file to server
|
# Upload file to server
|
||||||
with open(
|
with open(
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
"wb",
|
"wb",
|
||||||
) as f:
|
) as f:
|
||||||
f.write(file_binary)
|
f.write(file_binary)
|
||||||
|
|
@ -52,9 +52,9 @@ async def upload_content(
|
||||||
print("Uploading to s3 using boto3...")
|
print("Uploading to s3 using boto3...")
|
||||||
try:
|
try:
|
||||||
s3.upload_file(
|
s3.upload_file(
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
"learnhouse-media",
|
"learnhouse-media",
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
)
|
)
|
||||||
except ClientError as e:
|
except ClientError as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
@ -63,7 +63,7 @@ async def upload_content(
|
||||||
try:
|
try:
|
||||||
s3.head_object(
|
s3.head_object(
|
||||||
Bucket="learnhouse-media",
|
Bucket="learnhouse-media",
|
||||||
Key=f"content/{org_id}/{directory}/{file_and_format}",
|
Key=f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
)
|
)
|
||||||
print("File upload successful!")
|
print("File upload successful!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { cookies } from "next/headers";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||||
|
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string, activityid: string };
|
params: { orgslug: string, courseid: string, activityid: string };
|
||||||
|
|
@ -32,6 +33,7 @@ const EditActivity = async (params: any) => {
|
||||||
const activityid = params.params.activityid;
|
const activityid = params.params.activityid;
|
||||||
const courseid = params.params.courseid;
|
const courseid = params.params.courseid;
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
|
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||||
|
|
||||||
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
||||||
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
||||||
|
|
@ -40,7 +42,7 @@ const EditActivity = async (params: any) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
<EditorWrapper org={org} orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export async function generateMetadata(
|
||||||
const CollectionPage = async (params: any) => {
|
const CollectionPage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
|
const org = await getOrganizationContextInfo(params.params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
|
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
|
||||||
|
|
||||||
|
|
@ -64,7 +65,7 @@ const CollectionPage = async (params: any) => {
|
||||||
{col.courses.map((course: any) => (
|
{col.courses.map((course: any) => (
|
||||||
<div className="pr-8" key={course.course_uuid}>
|
<div className="pr-8" key={course.course_uuid}>
|
||||||
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_uuid))}>
|
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_uuid))}>
|
||||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_uuid, course.thumbnail)})` }}>
|
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWra
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
|
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||||
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
|
|
||||||
interface ActivityClientProps {
|
interface ActivityClientProps {
|
||||||
activityid: string;
|
activityid: string;
|
||||||
|
|
@ -27,6 +28,7 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
const orgslug = props.orgslug;
|
const orgslug = props.orgslug;
|
||||||
const activity = props.activity;
|
const activity = props.activity;
|
||||||
const course = props.course;
|
const course = props.course;
|
||||||
|
const org = useOrg() as any;
|
||||||
|
|
||||||
function getChapterName(chapterId: string) {
|
function getChapterName(chapterId: string) {
|
||||||
let chapterName = "";
|
let chapterName = "";
|
||||||
|
|
@ -47,7 +49,7 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
<div className="flex space-x-6">
|
<div className="flex space-x-6">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
|
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
|
||||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_uuid, course.course.thumbnail)}`} alt="" />
|
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course.course_uuid, course.course.thumbnail_image)}`} alt="" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col -space-y-1">
|
<div className="flex flex-col -space-y-1">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||||
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
|
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
|
||||||
import Avvvatars from "avvvatars-react";
|
import Avvvatars from "avvvatars-react";
|
||||||
import { getUser } from "@services/users/users";
|
import { getUser } from "@services/users/users";
|
||||||
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
|
|
||||||
const CourseClient = (props: any) => {
|
const CourseClient = (props: any) => {
|
||||||
const [user, setUser] = useState<any>({});
|
const [user, setUser] = useState<any>({});
|
||||||
|
|
@ -19,6 +20,7 @@ const CourseClient = (props: any) => {
|
||||||
const courseuuid = props.courseuuid;
|
const courseuuid = props.courseuuid;
|
||||||
const orgslug = props.orgslug;
|
const orgslug = props.orgslug;
|
||||||
const course = props.course;
|
const course = props.course;
|
||||||
|
const org = useOrg() as any;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function getLearningTags() {
|
function getLearningTags() {
|
||||||
|
|
@ -28,7 +30,6 @@ const CourseClient = (props: any) => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(course);
|
|
||||||
|
|
||||||
async function startCourseUI() {
|
async function startCourseUI() {
|
||||||
// Create activity
|
// Create activity
|
||||||
|
|
@ -57,7 +58,7 @@ const CourseClient = (props: any) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
}
|
}
|
||||||
, []);
|
, [org]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -73,10 +74,10 @@ const CourseClient = (props: any) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_uuid, course.thumbnail)})` }}>
|
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
|
<ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
|
||||||
|
|
||||||
<div className="flex flex-row pt-10">
|
<div className="flex flex-row pt-10">
|
||||||
<div className="course_metadata_left grow space-y-2">
|
<div className="course_metadata_left grow space-y-2">
|
||||||
|
|
@ -198,7 +199,7 @@ const CourseClient = (props: any) => {
|
||||||
}
|
}
|
||||||
{console.log(course)}
|
{console.log(course)}
|
||||||
|
|
||||||
{isCourseStarted() ? (
|
{isCourseStarted() ? (
|
||||||
<button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}>
|
<button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}>
|
||||||
Quit Course
|
Quit Course
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
"use client";
|
|
||||||
import React, { FC, 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({ courseuuid, courseid, subpage, params }: { courseid: any, courseuuid: string, subpage: string, params: any }) {
|
|
||||||
const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`, swrFetcher);
|
|
||||||
const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseuuid}/meta`, swrFetcher);
|
|
||||||
const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {});
|
|
||||||
const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {});
|
|
||||||
const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
|
|
||||||
// This function is a quick fix to transform the payload object from what was used before to the new and improved format
|
|
||||||
// The entire course edition frontend code will be remade in the future in a proper way.
|
|
||||||
const ConvertToNewAPIOrderUpdatePayload = (courseChaptersMetadata: any) => {
|
|
||||||
const old_format = courseChaptersMetadata
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
// Convert originalObject to the desired format
|
|
||||||
const convertedObject = {
|
|
||||||
"chapter_order_by_ids": old_format.chapterOrder.map((chapterId: string | number, chapterIndex: any) => {
|
|
||||||
const chapter = old_format.chapters[chapterId];
|
|
||||||
return {
|
|
||||||
"chapter_id": chapter.id,
|
|
||||||
"activities_order_by_ids": chapter.activityIds.map((activityId: any, activityIndex: any) => {
|
|
||||||
return {
|
|
||||||
"activity_id": activityIndex
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
return convertedObject
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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') {
|
|
||||||
let payload = ConvertToNewAPIOrderUpdatePayload(courseChaptersMetadata)
|
|
||||||
await updateChaptersMetadata(courseuuid, payload)
|
|
||||||
dispatchSavedContent({ type: 'saved_content' })
|
|
||||||
await mutate(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`)
|
|
||||||
await revalidateTags(['courses'], params.params.orgslug)
|
|
||||||
router.refresh()
|
|
||||||
}
|
|
||||||
else if (subpage.toString() === 'general') {
|
|
||||||
await updateCourse(courseuuid, courseState)
|
|
||||||
dispatchSavedContent({ type: 'saved_content' })
|
|
||||||
await mutate(`${getAPIUrl()}courses/course_${courseuuid}`)
|
|
||||||
await mutate(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`)
|
|
||||||
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/${courseuuid}`}>
|
|
||||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseuuid, 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/${courseuuid}/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/${courseuuid}/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 course={course} dispatchSavedContent={dispatchSavedContent} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseuuid={courseuuid} orgslug={params.params.orgslug} />
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CoursePageViewer = ({ subpage, course, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseuuid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any, course: any }) => {
|
|
||||||
|
|
||||||
if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0 && course) {
|
|
||||||
return <CourseEdition course={course} orgslug={orgslug} course_chapters_with_orders_and_activities={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} />
|
|
||||||
}
|
|
||||||
else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0 && course) {
|
|
||||||
return <CourseContentEdition course={course} orgslug={orgslug} course_chapters_with_orders_and_activities={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} />
|
|
||||||
}
|
|
||||||
else if (subpage.toString() === 'content' || subpage.toString() === 'general') {
|
|
||||||
return <Loading />
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return <ErrorUI />
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CourseEditClient
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
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';
|
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
|
||||||
|
|
||||||
type MetadataProps = {
|
|
||||||
params: { orgslug: string, courseuuid: string };
|
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function generateMetadata(
|
|
||||||
{ params }: MetadataProps,
|
|
||||||
): Promise<Metadata> {
|
|
||||||
const cookieStore = cookies();
|
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
|
||||||
|
|
||||||
|
|
||||||
// Get Org context information
|
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
|
||||||
const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `Edit Course - ` + course_meta.name,
|
|
||||||
description: course_meta.mini_description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function CourseEdit(params: any) {
|
|
||||||
const cookieStore = cookies();
|
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
|
||||||
let subpage = params.params.subpage ? params.params.subpage : 'general';
|
|
||||||
const course_meta = await getCourseMetadataWithAuthHeader(params.params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CourseEditClient params={params} subpage={subpage} courseid={course_meta.id} courseuuid={params.params.courseuuid} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default CourseEdit;
|
|
||||||
|
|
@ -1,320 +0,0 @@
|
||||||
"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, getOrganizationContextInfoWithoutCredentials } from "@services/organizations/orgs";
|
|
||||||
import Modal from "@components/StyledElements/Modal/Modal";
|
|
||||||
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
|
|
||||||
import { Folders, Hexagon, SaveIcon } from "lucide-react";
|
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
|
||||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
|
||||||
import { getAPIUrl } from "@services/config/config";
|
|
||||||
import { mutate } from "swr";
|
|
||||||
|
|
||||||
function CourseContentEdition(props: any) {
|
|
||||||
const router = useRouter();
|
|
||||||
// Initial Course Chapters State
|
|
||||||
const course_chapters_with_orders_and_activities = props.course_chapters_with_orders_and_activities;
|
|
||||||
|
|
||||||
// New Chapter Modal State
|
|
||||||
const [newChapterModal, setNewChapterModal] = useState(false) as any;
|
|
||||||
// New Activity Modal State
|
|
||||||
const [newActivityModal, setNewActivityModal] = useState(false) as any;
|
|
||||||
const [selectedChapterToAddActivityTo, setSelectedChapterToAddActivityTo] = useState("") as any;
|
|
||||||
|
|
||||||
// Check window availability
|
|
||||||
const [winReady, setwinReady] = useState(false);
|
|
||||||
const course = props.course;
|
|
||||||
const course_uuid = props.course ? props.course.course_uuid : ''
|
|
||||||
const orgslug = props.orgslug;
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setwinReady(true);
|
|
||||||
}, [course_uuid, orgslug]);
|
|
||||||
|
|
||||||
// get a list of chapters order by chapter order
|
|
||||||
const getChapters = () => {
|
|
||||||
const chapterOrder = course_chapters_with_orders_and_activities.chapterOrder ? course_chapters_with_orders_and_activities.chapterOrder : [];
|
|
||||||
return chapterOrder.map((chapterId: any) => {
|
|
||||||
const chapter = course_chapters_with_orders_and_activities.chapters[chapterId];
|
|
||||||
let activities = [];
|
|
||||||
if (course_chapters_with_orders_and_activities.activities) {
|
|
||||||
activities = chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId])
|
|
||||||
? chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId])
|
|
||||||
: [];
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
list: {
|
|
||||||
chapter: chapter,
|
|
||||||
activities: activities,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Submit new chapter
|
|
||||||
const submitChapter = async (chapter: any) => {
|
|
||||||
await createChapter(chapter);
|
|
||||||
|
|
||||||
mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true);
|
|
||||||
await revalidateTags(['courses'], orgslug);
|
|
||||||
router.refresh();
|
|
||||||
setNewChapterModal(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Submit new activity
|
|
||||||
const submitActivity = async (activity: any) => {
|
|
||||||
let org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 1800 });
|
|
||||||
await createActivity(activity, activity.chapterId, org.org_id);
|
|
||||||
mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`);
|
|
||||||
// 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(course_uuid, course_chapters_with_orders_and_activities);
|
|
||||||
await createFileActivity(file, type, activity, chapterId);
|
|
||||||
mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`);
|
|
||||||
// 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(course_uuid, course_chapters_with_orders_and_activities);
|
|
||||||
await createExternalVideoActivity(external_video_data, activity, chapterId);
|
|
||||||
mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`);
|
|
||||||
// await getCourseChapters();
|
|
||||||
setNewActivityModal(false);
|
|
||||||
await revalidateTags(['courses'], orgslug);
|
|
||||||
router.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteChapterUI = async (chapterId: any) => {
|
|
||||||
await deleteChapter(chapterId);
|
|
||||||
|
|
||||||
mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true);
|
|
||||||
// await getCourseChapters();
|
|
||||||
await revalidateTags(['courses'], orgslug);
|
|
||||||
router.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Modals
|
|
||||||
*/
|
|
||||||
|
|
||||||
const openNewActivityModal = async (chapterId: any) => {
|
|
||||||
setNewActivityModal(true);
|
|
||||||
setSelectedChapterToAddActivityTo(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(course_chapters_with_orders_and_activities.chapterOrder);
|
|
||||||
newChapterOrder.splice(source.index, 1);
|
|
||||||
newChapterOrder.splice(destination.index, 0, draggableId);
|
|
||||||
|
|
||||||
const newState = {
|
|
||||||
...course_chapters_with_orders_and_activities,
|
|
||||||
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 = course_chapters_with_orders_and_activities.chapters[source.droppableId];
|
|
||||||
const finish = course_chapters_with_orders_and_activities.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 = course_chapters_with_orders_and_activities.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 = {
|
|
||||||
...course_chapters_with_orders_and_activities,
|
|
||||||
chapters: {
|
|
||||||
...course_chapters_with_orders_and_activities.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 = {
|
|
||||||
...course_chapters_with_orders_and_activities,
|
|
||||||
chapters: {
|
|
||||||
...course_chapters_with_orders_and_activities.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={selectedChapterToAddActivityTo}
|
|
||||||
course={course}
|
|
||||||
></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}
|
|
||||||
course_uuid={course_uuid}
|
|
||||||
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
|
|
||||||
course={props.course ? props.course : null}
|
|
||||||
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">
|
|
||||||
<Hexagon size={16} />
|
|
||||||
<div>Add chapter +</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</GeneralWrapperStyled >
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default CourseContentEdition;
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
"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.course_chapters_with_orders_and_activities.name),
|
|
||||||
mini_description: String(props.course_chapters_with_orders_and_activities.mini_description),
|
|
||||||
description: String(props.course_chapters_with_orders_and_activities.description),
|
|
||||||
learnings: String(props.course_chapters_with_orders_and_activities.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.course_chapters_with_orders_and_activities,
|
|
||||||
name: formik.values.name,
|
|
||||||
description: formik.values.description,
|
|
||||||
learnings: formik.values.learnings,
|
|
||||||
};
|
|
||||||
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="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,5 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
import BreadCrumbs from '@components/DashboardPages/UI/BreadCrumbs'
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||||
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
|
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
|
||||||
import CourseThumbnail from '@components/Objects/Other/CourseThumbnail';
|
import CourseThumbnail from '@components/Objects/Other/CourseThumbnail';
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||||
|
|
@ -34,7 +34,7 @@ function CoursesHome(params: CourseProps) {
|
||||||
<BreadCrumbs type='courses' />
|
<BreadCrumbs type='courses' />
|
||||||
|
|
||||||
<div className='w-100 flex justify-between'>
|
<div className='w-100 flex justify-between'>
|
||||||
<div className='flex font-bold text-4xl'>Courses</div>
|
<div className='pt-3 flex font-bold text-4xl'>Courses</div>
|
||||||
<AuthenticatedClientElement checkMethod='roles'
|
<AuthenticatedClientElement checkMethod='roles'
|
||||||
action='create'
|
action='create'
|
||||||
ressourceType='course'
|
ressourceType='course'
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
'use client';
|
'use client';
|
||||||
import EditCourseStructure from '../../../../../../../../components/DashboardPages/EditCourseStructure/EditCourseStructure'
|
import EditCourseStructure from '../../../../../../../../components/Dashboard/EditCourseStructure/EditCourseStructure'
|
||||||
import BreadCrumbs from '@components/DashboardPages/UI/BreadCrumbs'
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||||
import ClientComponentSkeleton from '@components/Utils/ClientComp';
|
import ClientComponentSkeleton from '@components/Utils/ClientComp';
|
||||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
import React, { createContext, use, useEffect, useState } from 'react'
|
import React, { createContext, use, useEffect, useState } from 'react'
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CourseProvider, useCourse } from '../../../../../../../../components/DashboardPages/CourseContext';
|
import { CourseProvider, useCourse } from '../../../../../../../../components/Contexts/CourseContext';
|
||||||
import SaveState from '@components/DashboardPages/UI/SaveState';
|
import SaveState from '@components/Dashboard/UI/SaveState';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { CourseOverviewTop } from '@components/DashboardPages/UI/CourseOverviewTop';
|
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import EditCourseGeneral from '@components/DashboardPages/EditCourseGeneral/EditCourseGeneral';
|
import EditCourseGeneral from '@components/Dashboard/EditCourseGeneral/EditCourseGeneral';
|
||||||
import { GalleryVertical, GalleryVerticalEnd, Info } from 'lucide-react';
|
import { GalleryVertical, GalleryVerticalEnd, Info } from 'lucide-react';
|
||||||
|
|
||||||
export type CourseOverviewParams = {
|
export type CourseOverviewParams = {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import LeftMenu from '@components/DashboardPages/UI/LeftMenu'
|
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
|
||||||
import AuthProvider from '@components/Security/AuthProvider'
|
import AuthProvider from '@components/Security/AuthProvider'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
|
'use client';
|
||||||
|
import { OrgProvider } from "@components/Contexts/OrgContext";
|
||||||
|
import { getAPIUrl } from "@services/config/config";
|
||||||
|
import { swrFetcher } from "@services/utils/ts/requests";
|
||||||
import "@styles/globals.css";
|
import "@styles/globals.css";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
export default function RootLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
||||||
|
|
||||||
export default function RootLayout({ children, params }: { children: React.ReactNode , params:any}) {
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
|
<OrgProvider orgslug={params.orgslug}>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</OrgProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
apps/web/components/Contexts/OrgContext.tsx
Normal file
25
apps/web/components/Contexts/OrgContext.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
'use client';
|
||||||
|
import { getAPIUrl } from '@services/config/config';
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
|
import React, { useContext, useEffect } from 'react'
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const OrgContext = createContext({}) as any;
|
||||||
|
|
||||||
|
export function OrgProvider({ children, orgslug }: { children: React.ReactNode, orgslug: string }) {
|
||||||
|
const { data: org } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher);
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}, [org]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OrgContext.Provider value={org}>
|
||||||
|
{children}
|
||||||
|
</OrgContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOrg() {
|
||||||
|
return useContext(OrgContext);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { AlertTriangle } from 'lucide-react'
|
||||||
import * as Switch from '@radix-ui/react-switch';
|
import * as Switch from '@radix-ui/react-switch';
|
||||||
import * as Form from '@radix-ui/react-form';
|
import * as Form from '@radix-ui/react-form';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useCourse, useCourseDispatch } from '../CourseContext';
|
import { useCourse, useCourseDispatch } from '../../Contexts/CourseContext';
|
||||||
|
|
||||||
|
|
||||||
type EditCourseStructureProps = {
|
type EditCourseStructureProps = {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCourse } from '@components/DashboardPages/CourseContext';
|
import { useCourse } from '@components/Contexts/CourseContext';
|
||||||
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity';
|
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity';
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
|
|
@ -9,7 +9,7 @@ import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||||
import { createChapter, updateCourseOrderStructure } from '@services/courses/chapters';
|
import { createChapter, updateCourseOrderStructure } from '@services/courses/chapters';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { CourseStructureContext } from 'app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page';
|
import { CourseStructureContext } from 'app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page';
|
||||||
import { useCourse, useCourseDispatch } from '@components/DashboardPages/CourseContext';
|
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
|
||||||
import { Hexagon } from 'lucide-react';
|
import { Hexagon } from 'lucide-react';
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal';
|
||||||
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter';
|
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter';
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCourse } from '@components/DashboardPages/CourseContext'
|
import { useCourse } from '@components/Contexts/CourseContext'
|
||||||
import { Book, ChevronRight, User } from 'lucide-react'
|
import { Book, ChevronRight, User } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React, { use, useEffect } from 'react'
|
import React, { use, useEffect } from 'react'
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
import { useCourse } from "@components/DashboardPages/CourseContext";
|
import { useCourse } from "@components/Contexts/CourseContext";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import BreadCrumbs from "./BreadCrumbs";
|
import BreadCrumbs from "./BreadCrumbs";
|
||||||
import SaveState from "./SaveState";
|
import SaveState from "./SaveState";
|
||||||
import { CourseOverviewParams } from "app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page";
|
import { CourseOverviewParams } from "app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page";
|
||||||
|
import { getUriWithOrg } from "@services/config/config";
|
||||||
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
|
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
|
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any;
|
||||||
|
const org = useOrg() as any;
|
||||||
|
|
||||||
useEffect(() => { }
|
useEffect(() => { }
|
||||||
, [course])
|
, [course, org])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BreadCrumbs type='courses' last_breadcrumb={course.courseStructure.name} ></BreadCrumbs>
|
<BreadCrumbs type='courses' last_breadcrumb={course.courseStructure.name} ></BreadCrumbs>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='flex py-5 grow items-center'>
|
<div className='flex py-5 grow items-center'>
|
||||||
<div className="image rounded-lg shadow-md bg-gray-900 w-28 h-14"></div>
|
<Link href={getUriWithOrg(org?.slug, "") + `/course/${params.courseuuid}`}>
|
||||||
|
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, "course_" + params.courseuuid, course.courseStructure.thumbnail_image)}`} alt="" />
|
||||||
|
</Link>
|
||||||
<div className="flex flex-col course_metadata justify-center pl-5">
|
<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-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 className='text-black font-bold text-xl -mt-1 first-letter:uppercase'>{course.courseStructure.name}</div>
|
||||||
39
apps/web/components/Dashboard/UI/LeftMenu.tsx
Normal file
39
apps/web/components/Dashboard/UI/LeftMenu.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
'use client';
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||||
|
import { ArrowLeft, Book, Home } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import React, { use, useEffect } from 'react'
|
||||||
|
|
||||||
|
function LeftMenu() {
|
||||||
|
const org = useOrg() as any;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}
|
||||||
|
, [org])
|
||||||
|
|
||||||
|
|
||||||
|
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 flex-col space-y-5 items-center mx-auto'>
|
||||||
|
<ToolTip content={"Back to " + org?.name} slateBlack sideOffset={8} side='right' >
|
||||||
|
<Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link>
|
||||||
|
</ToolTip>
|
||||||
|
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
|
||||||
|
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
|
||||||
|
</ToolTip>
|
||||||
|
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
||||||
|
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><Book size={18} /></Link>
|
||||||
|
</ToolTip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeftMenu
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import { updateCourseOrderStructure } from '@services/courses/chapters';
|
import { updateCourseOrderStructure } from '@services/courses/chapters';
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
import { revalidateTags } from '@services/utils/ts/requests';
|
||||||
import { useCourse, useCourseDispatch } from '@components/DashboardPages/CourseContext'
|
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'
|
||||||
import { Check, SaveAllIcon, Timer } from 'lucide-react'
|
import { Check, SaveAllIcon, Timer } from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
|
||||||
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'>
|
|
||||||
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><Book size={18} /></Link>
|
|
||||||
</ToolTip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeftMenu
|
|
||||||
|
|
||||||
|
|
@ -45,6 +45,7 @@ interface Editor {
|
||||||
activity: any;
|
activity: any;
|
||||||
orgslug: string
|
orgslug: string
|
||||||
course: any;
|
course: any;
|
||||||
|
org: any;
|
||||||
setContent: (content: string) => void;
|
setContent: (content: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +147,7 @@ function Editor(props: Editor) {
|
||||||
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link target="_blank" href={`/course/${course_uuid}/edit`}>
|
<Link target="_blank" href={`/course/${course_uuid}/edit`}>
|
||||||
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.course.course.org_id, props.course.course.course_uuid, props.course.course.thumbnail)}`} alt=""></EditorInfoThumbnail>
|
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course.course_uuid, props.course.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
|
||||||
</Link>
|
</Link>
|
||||||
<EditorInfoDocName>
|
<EditorInfoDocName>
|
||||||
{" "}
|
{" "}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ interface EditorWrapperProps {
|
||||||
activity: any;
|
activity: any;
|
||||||
course: any
|
course: any
|
||||||
orgslug: string;
|
orgslug: string;
|
||||||
|
org: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorWrapper(props: EditorWrapperProps): JSX.Element {
|
function EditorWrapper(props: EditorWrapperProps): JSX.Element {
|
||||||
|
|
@ -49,7 +50,7 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element {
|
||||||
} else {
|
} else {
|
||||||
return <>
|
return <>
|
||||||
<Toast></Toast>
|
<Toast></Toast>
|
||||||
<Editor orgslug={props.orgslug} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
<Editor org={props.org} orgslug={props.orgslug} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
||||||
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
import { getUriWithOrg } from '@services/config/config'
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
|
|
@ -21,6 +22,7 @@ const removeCollectionPrefix = (collectionid: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollectionThumbnail(props: PropsType) {
|
function CollectionThumbnail(props: PropsType) {
|
||||||
|
const org = useOrg() as any;
|
||||||
return (
|
return (
|
||||||
<div className=''>
|
<div className=''>
|
||||||
<div className="flex flex-row space-x-4 inset-0 ring-1 ring-inset my-auto ring-black/10 rounded-xl shadow-xl relative w-[300px] h-[80px] bg-cover items-center justify-center bg-indigo-600 font-bold text-zinc-50" >
|
<div className="flex flex-row space-x-4 inset-0 ring-1 ring-inset my-auto ring-black/10 rounded-xl shadow-xl relative w-[300px] h-[80px] bg-cover items-center justify-center bg-indigo-600 font-bold text-zinc-50" >
|
||||||
|
|
@ -28,7 +30,7 @@ function CollectionThumbnail(props: PropsType) {
|
||||||
{props.collection.courses.slice(0, 2).map((course: any) => (
|
{props.collection.courses.slice(0, 2).map((course: any) => (
|
||||||
<>
|
<>
|
||||||
<Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_uuid))}>
|
<Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_uuid))}>
|
||||||
<div className="inset-0 rounded-full shadow-2xl bg-cover w-12 h-8 justify-center ring-indigo-800 ring-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.collection.org_id, course.course_uuid, course.thumbnail)})` }}>
|
<div className="inset-0 rounded-full shadow-2xl bg-cover w-12 h-8 justify-center ring-indigo-800 ring-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
import { getUriWithOrg } from '@services/config/config';
|
||||||
|
|
@ -8,7 +9,7 @@ import { revalidateTags } from '@services/utils/ts/requests';
|
||||||
import { FileEdit, X } from 'lucide-react';
|
import { FileEdit, X } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react'
|
import React, { use, useEffect } from 'react'
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
course: any,
|
course: any,
|
||||||
|
|
@ -22,6 +23,7 @@ function removeCoursePrefix(course_uuid: string) {
|
||||||
|
|
||||||
function CourseThumbnail(props: PropsType) {
|
function CourseThumbnail(props: PropsType) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const org = useOrg() as any;
|
||||||
|
|
||||||
async function deleteCourses(course_uuid: any) {
|
async function deleteCourses(course_uuid: any) {
|
||||||
await deleteCourseFromBackend(course_uuid);
|
await deleteCourseFromBackend(course_uuid);
|
||||||
|
|
@ -30,12 +32,16 @@ function CourseThumbnail(props: PropsType) {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}, [org]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<AdminEditsArea course={props.course} orgSlug={props.orgslug} courseId={props.course.course_uuid} deleteCourses={deleteCourses} />
|
<AdminEditsArea course={props.course} orgSlug={props.orgslug} courseId={props.course.course_uuid} deleteCourses={deleteCourses} />
|
||||||
<Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course.course_uuid))}>
|
<Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course.course_uuid))}>
|
||||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.course.org_id, props.course.course_uuid, props.course.thumbnail)})` }}>
|
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)})` }}>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-lg w-[250px] py-2">{props.course.name}</h2>
|
<h2 className="font-bold text-lg w-[250px] py-2">{props.course.name}</h2>
|
||||||
|
|
@ -45,10 +51,10 @@ function CourseThumbnail(props: PropsType) {
|
||||||
|
|
||||||
const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any, deleteCourses: any }) => {
|
const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any, deleteCourses: any }) => {
|
||||||
return (
|
return (
|
||||||
<AuthenticatedClientElement
|
<AuthenticatedClientElement
|
||||||
action="update"
|
action="update"
|
||||||
ressourceType="course"
|
ressourceType="course"
|
||||||
checkMethod='roles' orgId={props.course.org_id}>
|
checkMethod='roles' orgId={props.course.org_id}>
|
||||||
<div className="flex space-x-1 absolute justify-center mx-auto z-20 bottom-14 left-1/2 transform -translate-x-1/2">
|
<div className="flex space-x-1 absolute justify-center mx-auto z-20 bottom-14 left-1/2 transform -translate-x-1/2">
|
||||||
<Link href={getUriWithOrg(props.orgSlug, "/dash/courses/course/" + removeCoursePrefix(props.courseId) + "/general")}>
|
<Link href={getUriWithOrg(props.orgSlug, "/dash/courses/course/" + removeCoursePrefix(props.courseId) + "/general")}>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { OrderPayload } from "@components/DashboardPages/EditCourseStructure/EditCourseStructure";
|
import { OrderPayload } from "@components/Dashboard/EditCourseStructure/EditCourseStructure";
|
||||||
import { getAPIUrl } from "@services/config/config";
|
import { getAPIUrl } from "@services/config/config";
|
||||||
import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests";
|
import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue