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 React from 'react'
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 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="flex justify-center items-center">
<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"
>
<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">
Learn LearnHouse
LearnHouse University
</div>
</Link>
</div>

View file

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

View file

@ -7,7 +7,7 @@ import { unLinkResourcesToUserGroup } from '@services/usergroups/usergroups'
import { swrFetcher } from '@services/utils/ts/requests'
import { Globe, SquareUserRound, Users, X } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import React from 'react'
import React, { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import useSWR, { mutate } from 'swr'
@ -17,57 +17,60 @@ type EditCourseAccessProps = {
}
function EditCourseAccess(props: EditCourseAccessProps) {
const [error, setError] = React.useState('')
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const course = useCourse() as any;
const { isLoading, courseStructure } = course 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 dispatchCourse = useCourseDispatch() as any;
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(() => {
// This code will run whenever form values are updated
if ((isPublic !== courseStructure.public) && isLoading) {
dispatchCourse({ type: 'setIsNotSaved' })
useEffect(() => {
if (!isLoading && courseStructure?.public !== undefined) {
setIsClientPublic(courseStructure.public);
}
}, [isLoading, courseStructure]);
useEffect(() => {
if (!isLoading && courseStructure?.public !== undefined && isClientPublic !== undefined) {
if (isClientPublic !== courseStructure.public) {
dispatchCourse({ type: 'setIsNotSaved' });
const updatedCourse = {
...courseStructure,
public: isPublic,
public: isClientPublic,
};
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse });
}
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse })
}
}, [course, isPublic])
}, [isLoading, isClientPublic, courseStructure, dispatchCourse]);
return (
<div>
{' '}
{courseStructure && (
<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="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>
<h2 className="text-gray-500 text-sm">
{' '}
Choose if want your course to be publicly available on the internet or only accessible to signed in users{' '}
Choose if you want your course to be publicly available on the internet or only accessible to signed in users
</h2>
</div>
<div className="flex space-x-2 mx-auto mb-3">
<ConfirmationModal
confirmationButtonText="Change to Public"
confirmationMessage="Are you sure you want this course to be publicly available on the internet ?"
dialogTitle={'Change to Public ?'}
confirmationMessage="Are you sure you want this course to be publicly available on the internet?"
dialogTitle="Change to Public?"
dialogTrigger={
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
{isPublic ? (
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 transition-all">
{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">
Active
</div>
) : null}
)}
<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">
Public
</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
</div>
</div>
</div>
}
functionToExecute={() => {
setIsPublic(true)
}}
functionToExecute={() => setIsClientPublic(true)}
status="info"
></ConfirmationModal>
/>
<ConfirmationModal
confirmationButtonText="Change to Users Only"
confirmationMessage="Are you sure you want this course to be only accessible to signed in users ?"
dialogTitle={'Change to Users Only ?'}
confirmationMessage="Are you sure you want this course to be only accessible to signed in users?"
dialogTitle="Change to Users Only?"
dialogTrigger={
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
{!isPublic ? (
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 transition-all">
{!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">
Active
</div>
) : null}
)}
<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">
Users Only
</div>
<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>
}
functionToExecute={() => {
setIsPublic(false)
}}
functionToExecute={() => setIsClientPublic(false)}
status="info"
></ConfirmationModal>
/>
</div>
{!isPublic ? (<UserGroupsSection usergroups={usergroups} />) : null}
{!isClientPublic && <UserGroupsSection usergroups={usergroups} />}
</div>
</div>
)
)}
</div>
);
}
function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
const course = useCourse() as any
const [userGroupModal, setUserGroupModal] = React.useState(false)
const course = useCourse() as any;
const [userGroupModal, setUserGroupModal] = useState(false);
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
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) {
toast.success('Successfully unliked from usergroup')
mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`)
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
toast.success('Successfully unlinked from usergroup');
mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`);
} else {
toast.error(`Error ${res.status}: ${res.data.detail}`);
}
} catch (error) {
toast.error('An error occurred while unlinking the user group.');
}
};
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>
<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>
</div>
<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>
</tr>
</thead>
<>
<tbody className="mt-5 bg-white rounded-md">
{usergroups?.map((usergroup: any) => (
<tr
key={usergroup.invite_code_uuid}
className="border-b border-gray-100 text-sm"
>
<tr 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">
<ConfirmationModal
confirmationButtonText="Delete Link"
confirmationMessage="Users from this UserGroup will no longer have access to this course"
dialogTitle={'Unlink UserGroup ?'}
dialogTitle="Unlink UserGroup?"
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">
<X className="w-4 h-4" />
<span> Delete link</span>
<span>Delete link</span>
</button>
}
functionToExecute={() => {
removeUserGroupLink(usergroup.id)
}}
functionToExecute={() => removeUserGroupLink(usergroup.id)}
status="warning"
></ConfirmationModal>
/>
</td>
</tr>
))}
</tbody>
</>
</table>
<div className='flex flex-row-reverse mt-3 mr-2'>
<div className="flex flex-row-reverse mt-3 mr-2">
<Modal
isDialogOpen={
userGroupModal
}
onOpenChange={() =>
setUserGroupModal(!userGroupModal)
}
isDialogOpen={userGroupModal}
onOpenChange={() => setUserGroupModal(!userGroupModal)}
minHeight="no-min"
minWidth='md'
dialogContent={
<LinkToUserGroup setUserGroupModal={setUserGroupModal} />
}
minWidth="md"
dialogContent={<LinkToUserGroup setUserGroupModal={setUserGroupModal} />}
dialogTitle="Link Course to a UserGroup"
dialogDescription={
'Choose a UserGroup to link this course to, Users from this UserGroup will have access to this course.'
}
dialogDescription="Choose a UserGroup to link this course to. Users from this UserGroup will have access to this course."
dialogTrigger={
<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"
>
<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">
<SquareUserRound className="w-4 h-4" />
<span>Link to a UserGroup</span>
</button>
}
/>
</div>
</>
)
);
}
export default EditCourseAccess
export default EditCourseAccess;

View file

@ -5,6 +5,7 @@ import { revalidateTags } from '@services/utils/ts/requests'
import {
Eye,
File,
FilePenLine,
MoreVertical,
Pencil,
Save,
@ -44,7 +45,7 @@ function ActivityElement(props: ActivitiyElementProps) {
const activityUUID = props.activity.activity_uuid
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`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
@ -63,7 +64,7 @@ function ActivityElement(props: ActivitiyElementProps) {
content: props.activity.content,
}
await updateActivity(modifiedActivityCopy, activityUUID,access_token)
await updateActivity(modifiedActivityCopy, activityUUID, access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
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"
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>
</>
)}
@ -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"
>
<Eye strokeWidth={2} size={15} className="text-gray-600" />
<Eye strokeWidth={2} size={12} className="text-gray-600" />
<span>Preview</span>
</Link>
</div>
{/* Delete Button */}

View file

@ -2,10 +2,11 @@
import { useCourse } from '@components/Contexts/CourseContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
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 { swrFetcher } from '@services/utils/ts/requests';
import { Info } from 'lucide-react';
import Link from 'next/link';
import React, { useEffect } from 'react'
import toast from 'react-hot-toast';
import useSWR, { mutate } from 'swr'
@ -24,7 +25,7 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
const { data: usergroups } = useSWR(
courseStructure && org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
swrFetcher
(url) => swrFetcher(url, access_token)
)
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>
</div>
<div className='p-4 flex-row flex justify-between items-center'>
{usergroups?.length >= 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>
<select
onChange={(e) => setSelectedUserGroup(e.target.value)}
defaultValue={selectedUserGroup}
@ -67,7 +69,13 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
))}
</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'>
<button onClick={() => { handleLink() }} className='bg-green-700 text-white font-bold px-4 py-2 rounded-md shadow'>Link</button>
</div>