feat: various course edition improvements

This commit is contained in:
swve 2024-06-14 00:18:23 +01:00
parent 29219391ea
commit f524ddb51a
5 changed files with 179 additions and 183 deletions

View file

@ -1,7 +1,7 @@
import Image from 'next/image' import Image from 'next/image'
import React from 'react' import React from 'react'
import learnhousetextlogo from '../../../../public/learnhouse_logo.png' import learnhousetextlogo from '../../../../public/learnhouse_logo.png'
import { BookCopy, School, Settings, Users } from 'lucide-react' import { BookCopy, School, Settings, University, Users } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import AdminAuthorization from '@components/Security/AdminAuthorization' import AdminAuthorization from '@components/Security/AdminAuthorization'
@ -62,12 +62,13 @@ function DashboardHome() {
<div className="h-1 w-[100px] bg-neutral-200 rounded-full mx-auto"></div> <div className="h-1 w-[100px] bg-neutral-200 rounded-full mx-auto"></div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<Link <Link
href={'https://learn.learnhouse.io/'} href={'https://university.learnhouse.io/'}
target='_blank'
className="flex mt-[40px] bg-black space-x-2 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer" className="flex mt-[40px] bg-black space-x-2 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer"
> >
<BookCopy className=" text-gray-100" size={20}></BookCopy> <University className=" text-gray-100" size={20}></University>
<div className=" text-sm font-bold text-gray-100"> <div className=" text-sm font-bold text-gray-100">
Learn LearnHouse LearnHouse University
</div> </div>
</Link> </Link>
</div> </div>

View file

@ -34,8 +34,9 @@ export function CourseProvider({ children, courseuuid }: any) {
}, [courseStructureData]); }, [courseStructureData]);
if (error) return <div>Failed to load course structure</div>; if (error) return <div>Failed to load course structure</div>;
if (!courseStructureData) return <PageLoading/>; if (!courseStructureData) return <PageLoading />;
if (courseStructureData) {
return ( return (
<CourseContext.Provider value={state}> <CourseContext.Provider value={state}>
<CourseDispatchContext.Provider value={dispatch}> <CourseDispatchContext.Provider value={dispatch}>
@ -43,6 +44,7 @@ export function CourseProvider({ children, courseuuid }: any) {
</CourseDispatchContext.Provider> </CourseDispatchContext.Provider>
</CourseContext.Provider> </CourseContext.Provider>
) )
}
} }
export function useCourse() { export function useCourse() {

View file

@ -7,7 +7,7 @@ import { unLinkResourcesToUserGroup } from '@services/usergroups/usergroups'
import { swrFetcher } from '@services/utils/ts/requests' import { swrFetcher } from '@services/utils/ts/requests'
import { Globe, SquareUserRound, Users, X } from 'lucide-react' import { Globe, SquareUserRound, Users, X } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import React from 'react' import React, { useEffect, useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import useSWR, { mutate } from 'swr' import useSWR, { mutate } from 'swr'
@ -17,57 +17,60 @@ type EditCourseAccessProps = {
} }
function EditCourseAccess(props: EditCourseAccessProps) { function EditCourseAccess(props: EditCourseAccessProps) {
const [error, setError] = React.useState('')
const session = useLHSession() as any; const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const course = useCourse() as any; const course = useCourse() as any;
const { isLoading, courseStructure } = course as any; const { isLoading, courseStructure } = course as any;
const dispatchCourse = useCourseDispatch() as any const dispatchCourse = useCourseDispatch() as any;
const { data: usergroups } = useSWR(
courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null,
(url) => swrFetcher(url, access_token)
)
const [isPublic, setIsPublic] = React.useState(courseStructure.public)
const { data: usergroups } = useSWR(courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null, (url) => swrFetcher(url, access_token));
const [isClientPublic, setIsClientPublic] = useState<boolean | undefined>(undefined);
React.useEffect(() => { useEffect(() => {
// This code will run whenever form values are updated if (!isLoading && courseStructure?.public !== undefined) {
if ((isPublic !== courseStructure.public) && isLoading) { setIsClientPublic(courseStructure.public);
dispatchCourse({ type: 'setIsNotSaved' }) }
}, [isLoading, courseStructure]);
useEffect(() => {
if (!isLoading && courseStructure?.public !== undefined && isClientPublic !== undefined) {
if (isClientPublic !== courseStructure.public) {
dispatchCourse({ type: 'setIsNotSaved' });
const updatedCourse = { const updatedCourse = {
...courseStructure, ...courseStructure,
public: isPublic, public: isClientPublic,
};
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse });
} }
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse })
} }
}, [course, isPublic]) }, [isLoading, isClientPublic, courseStructure, dispatchCourse]);
return ( return (
<div> <div>
{' '} {courseStructure && (
<div>
<div className="h-6"></div> <div className="h-6"></div>
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4"> <div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4">
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 "> <div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3">
<h1 className="font-bold text-xl text-gray-800">Access to the course</h1> <h1 className="font-bold text-xl text-gray-800">Access to the course</h1>
<h2 className="text-gray-500 text-sm"> <h2 className="text-gray-500 text-sm">
{' '} Choose if you want your course to be publicly available on the internet or only accessible to signed in users
Choose if want your course to be publicly available on the internet or only accessible to signed in users{' '}
</h2> </h2>
</div> </div>
<div className="flex space-x-2 mx-auto mb-3"> <div className="flex space-x-2 mx-auto mb-3">
<ConfirmationModal <ConfirmationModal
confirmationButtonText="Change to Public" confirmationButtonText="Change to Public"
confirmationMessage="Are you sure you want this course to be publicly available on the internet ?" confirmationMessage="Are you sure you want this course to be publicly available on the internet?"
dialogTitle={'Change to Public ?'} dialogTitle="Change to Public?"
dialogTrigger={ dialogTrigger={
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all"> <div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 transition-all">
{isPublic ? ( {isClientPublic && (
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg"> <div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
Active Active
</div> </div>
) : null} )}
<div className="flex flex-col space-y-1 justify-center items-center h-full"> <div className="flex flex-col space-y-1 justify-center items-center h-full">
<Globe className="text-slate-400" size={40}></Globe> <Globe className="text-slate-400" size={40} />
<div className="text-2xl text-slate-700 font-bold"> <div className="text-2xl text-slate-700 font-bold">
Public Public
</div> </div>
@ -75,74 +78,71 @@ function EditCourseAccess(props: EditCourseAccessProps) {
The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone
</div> </div>
</div> </div>
</div> </div>
} }
functionToExecute={() => { functionToExecute={() => setIsClientPublic(true)}
setIsPublic(true)
}}
status="info" status="info"
></ConfirmationModal> />
<ConfirmationModal <ConfirmationModal
confirmationButtonText="Change to Users Only" confirmationButtonText="Change to Users Only"
confirmationMessage="Are you sure you want this course to be only accessible to signed in users ?" confirmationMessage="Are you sure you want this course to be only accessible to signed in users?"
dialogTitle={'Change to Users Only ?'} dialogTitle="Change to Users Only?"
dialogTrigger={ dialogTrigger={
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all"> <div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 transition-all">
{!isPublic ? ( {!isClientPublic && (
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg"> <div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
Active Active
</div> </div>
) : null} )}
<div className="flex flex-col space-y-1 justify-center items-center h-full"> <div className="flex flex-col space-y-1 justify-center items-center h-full">
<Users className="text-slate-400" size={40}></Users> <Users className="text-slate-400" size={40} />
<div className="text-2xl text-slate-700 font-bold"> <div className="text-2xl text-slate-700 font-bold">
Users Only Users Only
</div> </div>
<div className="text-gray-400 text-md tracking-tight w-[500px] leading-5 text-center"> <div className="text-gray-400 text-md tracking-tight w-[500px] leading-5 text-center">
The Course is only accessible to signed in users, additionaly you can choose which UserGroups can access this course The Course is only accessible to signed in users, additionally you can choose which UserGroups can access this course
</div> </div>
</div> </div>
</div> </div>
} }
functionToExecute={() => { functionToExecute={() => setIsClientPublic(false)}
setIsPublic(false)
}}
status="info" status="info"
></ConfirmationModal> />
</div> </div>
{!isPublic ? (<UserGroupsSection usergroups={usergroups} />) : null} {!isClientPublic && <UserGroupsSection usergroups={usergroups} />}
</div> </div>
</div> </div>
) )}
</div>
);
} }
function UserGroupsSection({ usergroups }: { usergroups: any[] }) { function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
const course = useCourse() as any const course = useCourse() as any;
const [userGroupModal, setUserGroupModal] = React.useState(false) const [userGroupModal, setUserGroupModal] = useState(false);
const session = useLHSession() as any; const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const removeUserGroupLink = async (usergroup_id: number) => { const removeUserGroupLink = async (usergroup_id: number) => {
const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid, access_token) try {
const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid, access_token);
if (res.status === 200) { if (res.status === 200) {
toast.success('Successfully unliked from usergroup') toast.success('Successfully unlinked from usergroup');
mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`) mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`);
} } else {
else { toast.error(`Error ${res.status}: ${res.data.detail}`);
toast.error('Error ' + res.status + ': ' + res.data.detail)
} }
} catch (error) {
toast.error('An error occurred while unlinking the user group.');
} }
};
return ( return (
<> <>
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 "> <div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3">
<h1 className="font-bold text-xl text-gray-800">UserGroups</h1> <h1 className="font-bold text-xl text-gray-800">UserGroups</h1>
<h2 className="text-gray-500 text-sm"> <h2 className="text-gray-500 text-sm">
{' '} You can choose to give access to this course to specific groups of users only by linking it to a UserGroup
You can choose to give access to this course to specific groups of users only by linking it to a UserGroup{' '}
</h2> </h2>
</div> </div>
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden"> <table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
@ -152,67 +152,48 @@ function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
<th className="py-3 px-4">Actions</th> <th className="py-3 px-4">Actions</th>
</tr> </tr>
</thead> </thead>
<>
<tbody className="mt-5 bg-white rounded-md"> <tbody className="mt-5 bg-white rounded-md">
{usergroups?.map((usergroup: any) => ( {usergroups?.map((usergroup: any) => (
<tr <tr key={usergroup.invite_code_uuid} className="border-b border-gray-100 text-sm">
key={usergroup.invite_code_uuid}
className="border-b border-gray-100 text-sm"
>
<td className="py-3 px-4">{usergroup.name}</td> <td className="py-3 px-4">{usergroup.name}</td>
<td className="py-3 px-4"> <td className="py-3 px-4">
<ConfirmationModal <ConfirmationModal
confirmationButtonText="Delete Link" confirmationButtonText="Delete Link"
confirmationMessage="Users from this UserGroup will no longer have access to this course" confirmationMessage="Users from this UserGroup will no longer have access to this course"
dialogTitle={'Unlink UserGroup ?'} dialogTitle="Unlink UserGroup?"
dialogTrigger={ dialogTrigger={
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100"> <button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
<X className="w-4 h-4" /> <X className="w-4 h-4" />
<span> Delete link</span> <span>Delete link</span>
</button> </button>
} }
functionToExecute={() => { functionToExecute={() => removeUserGroupLink(usergroup.id)}
removeUserGroupLink(usergroup.id)
}}
status="warning" status="warning"
></ConfirmationModal> />
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</>
</table> </table>
<div className='flex flex-row-reverse mt-3 mr-2'> <div className="flex flex-row-reverse mt-3 mr-2">
<Modal <Modal
isDialogOpen={ isDialogOpen={userGroupModal}
userGroupModal onOpenChange={() => setUserGroupModal(!userGroupModal)}
}
onOpenChange={() =>
setUserGroupModal(!userGroupModal)
}
minHeight="no-min" minHeight="no-min"
minWidth='md' minWidth="md"
dialogContent={ dialogContent={<LinkToUserGroup setUserGroupModal={setUserGroupModal} />}
<LinkToUserGroup setUserGroupModal={setUserGroupModal} />
}
dialogTitle="Link Course to a UserGroup" dialogTitle="Link Course to a UserGroup"
dialogDescription={ dialogDescription="Choose a UserGroup to link this course to. Users from this UserGroup will have access to this course."
'Choose a UserGroup to link this course to, Users from this UserGroup will have access to this course.'
}
dialogTrigger={ dialogTrigger={
<button <button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100">
className=" flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
>
<SquareUserRound className="w-4 h-4" /> <SquareUserRound className="w-4 h-4" />
<span>Link to a UserGroup</span> <span>Link to a UserGroup</span>
</button> </button>
} }
/> />
</div> </div>
</> </>
) );
} }
export default EditCourseAccess export default EditCourseAccess;

View file

@ -5,6 +5,7 @@ import { revalidateTags } from '@services/utils/ts/requests'
import { import {
Eye, Eye,
File, File,
FilePenLine,
MoreVertical, MoreVertical,
Pencil, Pencil,
Save, Save,
@ -44,7 +45,7 @@ function ActivityElement(props: ActivitiyElementProps) {
const activityUUID = props.activity.activity_uuid const activityUUID = props.activity.activity_uuid
async function deleteActivityUI() { async function deleteActivityUI() {
await deleteActivity(props.activity.activity_uuid,access_token) await deleteActivity(props.activity.activity_uuid, access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
@ -63,7 +64,7 @@ function ActivityElement(props: ActivitiyElementProps) {
content: props.activity.content, content: props.activity.content,
} }
await updateActivity(modifiedActivityCopy, activityUUID,access_token) await updateActivity(modifiedActivityCopy, activityUUID, access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
@ -144,7 +145,9 @@ function ActivityElement(props: ActivitiyElementProps) {
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center" className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<div className="text-sky-100 font-bold text-xs">Edit </div> <div className="text-sky-100 font-bold text-xs flex items-center space-x-1">
<FilePenLine size={12} /> <span>Edit Page</span>
</div>
</Link> </Link>
</> </>
)} )}
@ -159,10 +162,11 @@ function ActivityElement(props: ActivitiyElementProps) {
'' ''
)}` )}`
} }
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md" className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md font-bold text-xs flex items-center space-x-1"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Eye strokeWidth={2} size={15} className="text-gray-600" /> <Eye strokeWidth={2} size={12} className="text-gray-600" />
<span>Preview</span>
</Link> </Link>
</div> </div>
{/* Delete Button */} {/* Delete Button */}

View file

@ -2,10 +2,11 @@
import { useCourse } from '@components/Contexts/CourseContext'; import { useCourse } from '@components/Contexts/CourseContext';
import { useLHSession } from '@components/Contexts/LHSessionContext'; import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext'; import { useOrg } from '@components/Contexts/OrgContext';
import { getAPIUrl } from '@services/config/config'; import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { linkResourcesToUserGroup } from '@services/usergroups/usergroups'; import { linkResourcesToUserGroup } from '@services/usergroups/usergroups';
import { swrFetcher } from '@services/utils/ts/requests'; import { swrFetcher } from '@services/utils/ts/requests';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import Link from 'next/link';
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import useSWR, { mutate } from 'swr' import useSWR, { mutate } from 'swr'
@ -24,7 +25,7 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
const { data: usergroups } = useSWR( const { data: usergroups } = useSWR(
courseStructure && org ? `${getAPIUrl()}usergroups/org/${org.id}` : null, courseStructure && org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
swrFetcher (url) => swrFetcher(url, access_token)
) )
const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any
@ -55,9 +56,10 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
<h1 className=' font-medium'>Users that are not part of the UserGroup will no longer have access to this course</h1> <h1 className=' font-medium'>Users that are not part of the UserGroup will no longer have access to this course</h1>
</div> </div>
<div className='p-4 flex-row flex justify-between items-center'> <div className='p-4 flex-row flex justify-between items-center'>
{usergroups?.length >= 1 &&
<div className='py-1'> <div className='py-1'>
<span className='px-3 text-gray-400 font-bold rounded-full py-1 bg-gray-100 mx-3'>UserGroup Name </span> <span className='px-3 text-gray-400 font-bold rounded-full py-1 bg-gray-100 mx-3'>UserGroup Name </span>
<select <select
onChange={(e) => setSelectedUserGroup(e.target.value)} onChange={(e) => setSelectedUserGroup(e.target.value)}
defaultValue={selectedUserGroup} defaultValue={selectedUserGroup}
@ -67,7 +69,13 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
))} ))}
</select> </select>
</div>
</div>}
{usergroups?.length == 0 &&
<div className='flex space-x-3 items-center'>
<span className='px-3 text-yellow-700 font-bold rounded-full py-1 mx-3'>No UserGroups available </span>
<Link className='px-3 text-blue-700 font-bold rounded-full py-1 bg-blue-100 mx-1' target='_blank' href={getUriWithOrg(org.slug, '/dash/users/settings/usergroups')}>Create a UserGroup</Link>
</div>}
<div className='py-3'> <div className='py-3'>
<button onClick={() => { handleLink() }} className='bg-green-700 text-white font-bold px-4 py-2 rounded-md shadow'>Link</button> <button onClick={() => { handleLink() }} className='bg-green-700 text-white font-bold px-4 py-2 rounded-md shadow'>Link</button>
</div> </div>