Merge pull request #268 from learnhouse/feat/june-bugs-cleaning-season

June Bugs Cleaning 🪲
This commit is contained in:
Badr B 2024-06-17 19:14:05 +01:00 committed by GitHub
commit 6390c56282
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 446 additions and 403 deletions

View file

@ -34,7 +34,6 @@ async def authorization_verify_if_element_is_public(
) )
if element_nature == "collections" and action == "read": if element_nature == "collections" and action == "read":
statement = select(Collection).where( statement = select(Collection).where(
Collection.public == True, Collection.collection_uuid == element_uuid Collection.public == True, Collection.collection_uuid == element_uuid
) )

View file

@ -28,7 +28,7 @@ from fastapi import HTTPException, status, Request
async def get_collection( async def get_collection(
request: Request, request: Request,
collection_uuid: str, collection_uuid: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
) -> CollectionRead: ) -> CollectionRead:
statement = select(Collection).where(Collection.collection_uuid == collection_uuid) statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
@ -48,6 +48,7 @@ async def get_collection(
statement_all = ( statement_all = (
select(Course) select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id) .join(CollectionCourse, Course.id == CollectionCourse.course_id)
.where(CollectionCourse.org_id == collection.org_id)
.distinct(Course.id) .distinct(Course.id)
) )
@ -57,7 +58,7 @@ async def get_collection(
.where(CollectionCourse.org_id == collection.org_id, Course.public == True) .where(CollectionCourse.org_id == collection.org_id, Course.public == True)
) )
if current_user.id == 0: if current_user.user_uuid == "user_anonymous":
statement = statement_public statement = statement_public
else: else:
statement = statement_all statement = statement_all
@ -88,7 +89,6 @@ async def create_collection(
# Add collection to database # Add collection to database
db_session.add(collection) db_session.add(collection)
db_session.commit() db_session.commit()
db_session.refresh(collection) db_session.refresh(collection)
# Link courses to collection # Link courses to collection
@ -184,6 +184,7 @@ async def update_collection(
statement = ( statement = (
select(Course) select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id) .join(CollectionCourse, Course.id == CollectionCourse.course_id)
.where(Course.org_id == collection.org_id)
.distinct(Course.id) .distinct(Course.id)
) )
@ -255,6 +256,7 @@ async def get_collections(
statement_all = ( statement_all = (
select(Course) select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id) .join(CollectionCourse, Course.id == CollectionCourse.course_id)
.where(CollectionCourse.org_id == collection.org_id)
.distinct(Course.id) .distinct(Course.id)
) )
statement_public = ( statement_public = (
@ -297,9 +299,11 @@ async def rbac_check(
detail="User rights : You are not allowed to read this collection", detail="User rights : You are not allowed to read this collection",
) )
else: else:
res = await authorization_verify_based_on_roles_and_authorship_and_usergroups( res = (
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
request, current_user.id, action, collection_uuid, db_session request, current_user.id, action, collection_uuid, db_session
) )
)
return res return res
else: else:
await authorization_verify_if_user_is_anon(current_user.id) await authorization_verify_if_user_is_anon(current_user.id)

View file

@ -42,6 +42,7 @@ function ForgotPasswordClient() {
email: '' email: ''
}, },
validate, validate,
validateOnBlur: true,
onSubmit: async (values) => { onSubmit: async (values) => {
setIsSubmitting(true) setIsSubmitting(true)
let res = await sendResetLink(values.email, org?.id) let res = await sendResetLink(values.email, org?.id)

View file

@ -51,8 +51,17 @@ const LoginClient = (props: LoginClientProps) => {
password: '', password: '',
}, },
validate, validate,
onSubmit: async (values) => { validateOnBlur: true,
validateOnChange: true,
onSubmit: async (values, {validateForm, setErrors, setSubmitting}) => {
setIsSubmitting(true) setIsSubmitting(true)
const errors = await validateForm(values);
if (Object.keys(errors).length > 0) {
setErrors(errors);
setSubmitting(false);
return;
}
const res = await signIn('credentials', { const res = await signIn('credentials', {
redirect: false, redirect: false,
email: values.email, email: values.email,
@ -139,7 +148,7 @@ const LoginClient = (props: LoginClientProps) => {
onChange={formik.handleChange} onChange={formik.handleChange}
value={formik.values.email} value={formik.values.email}
type="email" type="email"
required
/> />
</Form.Control> </Form.Control>
</FormField> </FormField>
@ -155,7 +164,7 @@ const LoginClient = (props: LoginClientProps) => {
onChange={formik.handleChange} onChange={formik.handleChange}
value={formik.values.password} value={formik.values.password}
type="password" type="password"
required
/> />
</Form.Control> </Form.Control>
</FormField> </FormField>

View file

@ -49,6 +49,7 @@ const EditActivity = async (params: any) => {
{ revalidate: 0, tags: ['activities'] }, { revalidate: 0, tags: ['activities'] },
access_token ? access_token : null access_token ? access_token : null
) )
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { const org = await getOrganizationContextInfoWithId(courseInfo.org_id, {
revalidate: 180, revalidate: 180,
tags: ['organizations'], tags: ['organizations'],

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

@ -1,5 +1,6 @@
'use client' 'use client'
import { OrgProvider } from '@components/Contexts/OrgContext' import { OrgProvider } from '@components/Contexts/OrgContext'
import NextTopLoader from 'nextjs-toploader';
import Toast from '@components/StyledElements/Toast/Toast' import Toast from '@components/StyledElements/Toast/Toast'
import '@styles/globals.css' import '@styles/globals.css'
@ -13,6 +14,7 @@ export default function RootLayout({
return ( return (
<div> <div>
<OrgProvider orgslug={params.orgslug}> <OrgProvider orgslug={params.orgslug}>
<NextTopLoader color="#2e2e2e" initialPosition={0.3} height={4} easing={'ease'} speed={500} showSpinner={false} />
<Toast /> <Toast />
{children} {children}
</OrgProvider> </OrgProvider>

View file

@ -4,6 +4,7 @@ import { swrFetcher } from '@services/utils/ts/requests'
import React, { createContext, useContext, useEffect, useReducer } from 'react' import React, { createContext, useContext, useEffect, useReducer } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import PageLoading from '@components/Objects/Loaders/PageLoading'
export const CourseContext = createContext(null) export const CourseContext = createContext(null)
export const CourseDispatchContext = createContext(null) export const CourseDispatchContext = createContext(null)
@ -33,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 <div>Loading...</div>; if (!courseStructureData) return '';
if (courseStructureData) {
return ( return (
<CourseContext.Provider value={state}> <CourseContext.Provider value={state}>
<CourseDispatchContext.Provider value={dispatch}> <CourseDispatchContext.Provider value={dispatch}>
@ -43,6 +45,7 @@ export function CourseProvider({ children, courseuuid }: any) {
</CourseContext.Provider> </CourseContext.Provider>
) )
} }
}
export function useCourse() { export function useCourse() {
return useContext(CourseContext) return useContext(CourseContext)

View file

@ -30,7 +30,7 @@ export function OrgProvider({ children, orgslug }: { children: React.ReactNode,
const isUserPartOfTheOrg = useMemo(() => orgs?.some((userOrg: any) => userOrg.id === org?.id), [orgs, org?.id]) const isUserPartOfTheOrg = useMemo(() => orgs?.some((userOrg: any) => userOrg.id === org?.id), [orgs, org?.id])
if (orgError || orgsError) return <ErrorUI message='An error occurred while fetching data' /> if (orgError || orgsError) return <ErrorUI message='An error occurred while fetching data' />
if (!org || !orgs || !session) return <div>Loading...</div> if (!org || !orgs || !session) return <div></div>
if (!isOrgActive) return <ErrorUI message='This organization is no longer active' /> if (!isOrgActive) return <ErrorUI message='This organization is no longer active' />
if (!isUserPartOfTheOrg && session.status == 'authenticated' && !isAllowedPathname) { if (!isUserPartOfTheOrg && session.status == 'authenticated' && !isAllowedPathname) {
return ( return (

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,
@ -141,10 +142,13 @@ function ActivityElement(props: ActivitiyElementProps) {
'' ''
)}/edit` )}/edit`
} }
prefetch
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" target='_blank' // hotfix for an editor prosemirror bug
> >
<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 +163,12 @@ function ActivityElement(props: ActivitiyElementProps) {
'' ''
)}` )}`
} }
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md" prefetch
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

@ -47,6 +47,7 @@ import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
import Collaboration from '@tiptap/extension-collaboration' import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import ActiveAvatars from './ActiveAvatars' import ActiveAvatars from './ActiveAvatars'
import { getUriWithOrg } from '@services/config/config'
interface Editor { interface Editor {
content: string content: string
@ -182,11 +183,11 @@ function Editor(props: Editor) {
</Link> </Link>
<Link target="_blank" href={`/course/${course_uuid}`}> <Link target="_blank" href={`/course/${course_uuid}`}>
<EditorInfoThumbnail <EditorInfoThumbnail
src={`${getCourseThumbnailMediaDirectory( src={`${props.course.thumbnail_image ? getCourseThumbnailMediaDirectory(
props.org?.org_uuid, props.org?.org_uuid,
props.course.course_uuid, props.course.course_uuid,
props.course.thumbnail_image props.course.thumbnail_image
)}`} ) : getUriWithOrg(props.org?.slug,'/empty_thumbnail.png')}`}
alt="" alt=""
></EditorInfoThumbnail> ></EditorInfoThumbnail>
</Link> </Link>

View file

@ -1,12 +1,13 @@
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement' import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import { getUriWithOrg } from '@services/config/config' import { getUriWithOrg } from '@services/config/config'
import { BookCopy, Signpost, SquareLibrary } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React from 'react'
function MenuLinks(props: { orgslug: string }) { function MenuLinks(props: { orgslug: string }) {
return ( return (
<div> <div className='pl-1'>
<ul className="flex space-x-4"> <ul className="flex space-x-5">
<LinkItem <LinkItem
link="/courses" link="/courses"
type="courses" type="courses"
@ -33,57 +34,24 @@ const LinkItem = (props: any) => {
const orgslug = props.orgslug const orgslug = props.orgslug
return ( return (
<Link href={getUriWithOrg(orgslug, link)}> <Link href={getUriWithOrg(orgslug, link)}>
<li className="flex space-x-3 items-center text-[#909192] font-medium"> <li className="flex space-x-2 items-center text-[#909192] font-medium">
{props.type == 'courses' && ( {props.type == 'courses' && (
<> <>
<svg <BookCopy size={20} />{' '}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.9987 1.66663H6.66536C5.78131 1.66663 4.93346 2.01782 4.30834 2.64294C3.68322 3.26806 3.33203 4.1159 3.33203 4.99996V15C3.33203 15.884 3.68322 16.7319 4.30834 17.357C4.93346 17.9821 5.78131 18.3333 6.66536 18.3333H14.9987C15.4407 18.3333 15.8646 18.1577 16.1772 17.8451C16.4898 17.5326 16.6654 17.1087 16.6654 16.6666V3.33329C16.6654 2.89127 16.4898 2.46734 16.1772 2.15478C15.8646 1.84222 15.4407 1.66663 14.9987 1.66663ZM4.9987 4.99996C4.9987 4.55793 5.17429 4.13401 5.48685 3.82145C5.79941 3.50889 6.22334 3.33329 6.66536 3.33329H14.9987V11.6666H6.66536C6.0779 11.6691 5.50203 11.8303 4.9987 12.1333V4.99996ZM6.66536 16.6666C6.22334 16.6666 5.79941 16.491 5.48685 16.1785C5.17429 15.8659 4.9987 15.442 4.9987 15C4.9987 14.5579 5.17429 14.134 5.48685 13.8214C5.79941 13.5089 6.22334 13.3333 6.66536 13.3333H14.9987V16.6666H6.66536ZM8.33203 6.66663H11.6654C11.8864 6.66663 12.0983 6.57883 12.2546 6.42255C12.4109 6.26627 12.4987 6.05431 12.4987 5.83329C12.4987 5.61228 12.4109 5.40032 12.2546 5.24404C12.0983 5.08776 11.8864 4.99996 11.6654 4.99996H8.33203C8.11102 4.99996 7.89906 5.08776 7.74278 5.24404C7.5865 5.40032 7.4987 5.61228 7.4987 5.83329C7.4987 6.05431 7.5865 6.26627 7.74278 6.42255C7.89906 6.57883 8.11102 6.66663 8.33203 6.66663V6.66663Z"
fill="#898A8B"
/>
</svg>
<span>Courses</span> <span>Courses</span>
</> </>
)} )}
{props.type == 'collections' && ( {props.type == 'collections' && (
<> <>
<svg <SquareLibrary size={20} />{' '}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.0567 6.14171C17.0567 6.14171 17.0567 6.14171 17.0567 6.07504L17.0067 5.95004C16.9893 5.92352 16.9698 5.89844 16.9483 5.87504C16.926 5.83976 16.901 5.80632 16.8733 5.77504L16.7983 5.71671L16.665 5.65004L10.415 1.79171C10.2826 1.70893 10.1295 1.66504 9.97333 1.66504C9.81715 1.66504 9.66411 1.70893 9.53166 1.79171L3.33166 5.65004L3.25666 5.71671L3.18166 5.77504C3.15404 5.80632 3.12896 5.83976 3.10666 5.87504C3.08524 5.89844 3.06573 5.92352 3.04833 5.95004L2.99833 6.07504C2.99833 6.07504 2.99833 6.07504 2.99833 6.14171C2.99014 6.2137 2.99014 6.28639 2.99833 6.35838V13.6417C2.99805 13.7833 3.03386 13.9227 3.10239 14.0466C3.17092 14.1706 3.2699 14.275 3.39 14.35L9.64 18.2084C9.67846 18.2321 9.72076 18.2491 9.765 18.2584C9.765 18.2584 9.80666 18.2584 9.83166 18.2584C9.97265 18.3031 10.124 18.3031 10.265 18.2584C10.265 18.2584 10.3067 18.2584 10.3317 18.2584C10.3759 18.2491 10.4182 18.2321 10.4567 18.2084L16.665 14.35C16.7851 14.275 16.8841 14.1706 16.9526 14.0466C17.0211 13.9227 17.0569 13.7833 17.0567 13.6417V6.35838C17.0649 6.28639 17.0649 6.2137 17.0567 6.14171ZM9.165 16.0084L4.58166 13.175V7.85838L9.165 10.6834V16.0084ZM9.99833 9.24171L5.33166 6.35838L9.99833 3.48337L14.665 6.35838L9.99833 9.24171ZM15.415 13.175L10.8317 16.0084V10.6834L15.415 7.85838V13.175Z"
fill="#898A8B"
/>
</svg>
<span>Collections</span> <span>Collections</span>
</> </>
)} )}
{props.type == 'trail' && ( {props.type == 'trail' && (
<> <>
<svg <Signpost size={20} />{' '}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.5751 7.95841C16.5059 7.82098 16.3999 7.70541 16.269 7.62451C16.1381 7.54361 15.9874 7.50054 15.8335 7.50008H11.6668V2.50008C11.6757 2.31731 11.6243 2.13669 11.5204 1.98608C11.4164 1.83547 11.2658 1.72325 11.0918 1.66674C10.9245 1.6117 10.744 1.61108 10.5763 1.66498C10.4087 1.71888 10.2624 1.82452 10.1585 1.96674L3.4918 11.1334C3.40827 11.2541 3.35811 11.3948 3.3464 11.5411C3.3347 11.6874 3.36186 11.8343 3.42513 11.9667C3.4834 12.1182 3.58462 12.2493 3.71637 12.3441C3.84812 12.4388 4.00467 12.493 4.1668 12.5001H8.33346V17.5001C8.33359 17.6758 8.38927 17.847 8.49254 17.9892C8.59581 18.1314 8.74139 18.2373 8.90846 18.2917C8.99219 18.3177 9.07915 18.3317 9.1668 18.3334C9.29828 18.3338 9.42799 18.303 9.5453 18.2436C9.66262 18.1842 9.76422 18.0979 9.8418 17.9917L16.5085 8.82508C16.5982 8.70074 16.652 8.55404 16.6637 8.40112C16.6755 8.24821 16.6448 8.09502 16.5751 7.95841ZM10.0001 14.9334V11.6667C10.0001 11.4457 9.91233 11.2338 9.75605 11.0775C9.59977 10.9212 9.38781 10.8334 9.1668 10.8334H5.83346L10.0001 5.06674V8.33341C10.0001 8.55442 10.0879 8.76638 10.2442 8.92267C10.4005 9.07895 10.6124 9.16674 10.8335 9.16674H14.1668L10.0001 14.9334Z"
fill="#909192"
/>
</svg>
<span>Trail</span> <span>Trail</span>
</> </>
)} )}

View file

@ -59,7 +59,7 @@ function DynamicCanvaModal({ submitActivity, chapterId, course }: any) {
</FormMessage> </FormMessage>
</Flex> </Flex>
<Form.Control asChild> <Form.Control asChild>
<Textarea onChange={handleActivityDescriptionChange} required /> <Textarea onChange={handleActivityDescriptionChange} />
</Form.Control> </Form.Control>
</FormField> </FormField>

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>

View file

@ -1,5 +1,5 @@
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 { createInviteCode, createInviteCodeWithUserGroup } from '@services/organizations/invites' import { createInviteCode, createInviteCodeWithUserGroup } from '@services/organizations/invites'
import { swrFetcher } from '@services/utils/ts/requests' import { swrFetcher } from '@services/utils/ts/requests'
import { Ticket } from 'lucide-react' import { Ticket } from 'lucide-react'
@ -7,6 +7,7 @@ import { useLHSession } from '@components/Contexts/LHSessionContext'
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'
import Link from 'next/link'
type OrgInviteCodeGenerateProps = { type OrgInviteCodeGenerateProps = {
setInvitesModal: any setInvitesModal: any
@ -17,9 +18,10 @@ function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
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 [usergroup_id, setUsergroup_id] = React.useState(0); const [usergroup_id, setUsergroup_id] = React.useState(0);
const { data: usergroups } = useSWR( const { data: usergroups } = useSWR(
org ? `${getAPIUrl()}usergroups/org/${org.id}` : null, org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
swrFetcher (url) => swrFetcher(url, access_token)
) )
async function createInviteWithUserGroup() { async function createInviteWithUserGroup() {
@ -55,6 +57,8 @@ function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
<h1 className='mx-auto pt-4 text-gray-600 font-medium'>Invite Code linked to a UserGroup</h1> <h1 className='mx-auto pt-4 text-gray-600 font-medium'>Invite Code linked to a UserGroup</h1>
<h2 className='mx-auto text-xs text-gray-600 font-medium'>On Signup, Users will be automatically linked to a UserGroup of your choice</h2> <h2 className='mx-auto text-xs text-gray-600 font-medium'>On Signup, Users will be automatically linked to a UserGroup of your choice</h2>
<div className='flex items-center space-x-4 pt-3 mx-auto'> <div className='flex items-center space-x-4 pt-3 mx-auto'>
{usergroups?.length >= 1 &&
<div className='flex space-x-4 items-center'>
<select <select
defaultValue={usergroup_id} defaultValue={usergroup_id}
className='flex p-2 w-fit rounded-md text-sm bg-gray-100'> className='flex p-2 w-fit rounded-md text-sm bg-gray-100'>
@ -63,7 +67,9 @@ function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
{usergroup.name} {usergroup.name}
</option> </option>
))} ))}
</select> </select>
<div className=''> <div className=''>
<button <button
onClick={createInviteWithUserGroup} onClick={createInviteWithUserGroup}
@ -73,6 +79,12 @@ function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
<span> Generate </span> <span> Generate </span>
</button> </button>
</div> </div>
</div>}
{usergroups?.length == 0 &&
<div className='flex space-x-3 items-center text-xs pt-3'>
<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> </div>
</div> </div>
</div> </div>

View file

@ -4,6 +4,7 @@ import FormLayout, {
Flex, Flex,
FormField, FormField,
FormLabel, FormLabel,
FormLabelAndMessage,
FormMessage, FormMessage,
Input, Input,
} from '@components/StyledElements/Form/Form' } from '@components/StyledElements/Form/Form'
@ -15,37 +16,37 @@ import { createUserGroup } from '@services/usergroups/usergroups'
import { mutate } from 'swr' import { mutate } from 'swr'
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useFormik } from 'formik'
type AddUserGroupProps = { type AddUserGroupProps = {
setCreateUserGroupModal: any setCreateUserGroupModal: any
} }
const validate = (values: any) => {
const errors: any = {}
if (!values.name) {
errors.name = 'Name is Required'
}
return errors
}
function AddUserGroup(props: AddUserGroupProps) { function AddUserGroup(props: AddUserGroupProps) {
const org = useOrg() as any; const org = useOrg() as any;
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 [userGroupName, setUserGroupName] = React.useState('')
const [userGroupDescription, setUserGroupDescription] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false)
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { const formik = useFormik({
setUserGroupName(event.target.value) initialValues: {
} name: '',
description: '',
const handleDescriptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUserGroupDescription(event.target.value)
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setIsSubmitting(true)
const obj = {
name: userGroupName,
description: userGroupDescription,
org_id: org.id org_id: org.id
} },
const res = await createUserGroup(obj, access_token) validate,
onSubmit: async (values) => {
setIsSubmitting(true)
const res = await createUserGroup(values, access_token)
if (res.status == 200) { if (res.status == 200) {
setIsSubmitting(false) setIsSubmitting(false)
mutate(`${getAPIUrl()}usergroups/org/${org.id}`) mutate(`${getAPIUrl()}usergroups/org/${org.id}`)
@ -54,47 +55,45 @@ function AddUserGroup(props: AddUserGroupProps) {
} else { } else {
setIsSubmitting(false) setIsSubmitting(false)
} }
} },
})
return ( return (
<FormLayout onSubmit={handleSubmit}> <FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name"> <FormField name="name">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}> <FormLabelAndMessage
<FormLabel>Name</FormLabel> label="Name"
<FormMessage match="valueMissing"> message={formik.errors.name}
Please provide a ug name />
</FormMessage>
</Flex>
<Form.Control asChild> <Form.Control asChild>
<Input onChange={handleNameChange} type="text" required /> <Input
onChange={formik.handleChange}
value={formik.values.name}
type="name"
required
/>
</Form.Control> </Form.Control>
</FormField> </FormField>
<FormField name="description"> <FormField name="description">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}> <FormLabelAndMessage
<FormLabel>Description</FormLabel> label="Description"
<FormMessage match="valueMissing"> message={formik.errors.description}
Please provide a ug description />
</FormMessage>
</Flex>
<Form.Control asChild> <Form.Control asChild>
<Input onChange={handleDescriptionChange} type="text" required /> <Input
onChange={formik.handleChange}
value={formik.values.description}
type="description"
/>
</Form.Control> </Form.Control>
</FormField> </FormField>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}> <div className="flex py-4">
<Form.Submit asChild> <Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}> <button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
{isSubmitting ? ( {isSubmitting ? 'Loading...' : 'Create a UserGroup'}
<BarLoader </button>
cssOverride={{ borderRadius: 60 }}
width={60}
color="#ffffff"
/>
) : (
'Create UserGroup'
)}
</ButtonBlack>
</Form.Submit> </Form.Submit>
</Flex> </div>
</FormLayout> </FormLayout>
) )
} }

View file

@ -19,11 +19,11 @@ function ManageUsers(props: ManageUsersProps) {
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const { data: OrgUsers } = useSWR( const { data: OrgUsers } = useSWR(
org ? `${getAPIUrl()}orgs/${org.id}/users` : null, org ? `${getAPIUrl()}orgs/${org.id}/users` : null,
swrFetcher (url) => swrFetcher(url, access_token)
) )
const { data: UGusers } = useSWR( const { data: UGusers } = useSWR(
org ? `${getAPIUrl()}usergroups/${props.usergroup_id}/users` : null, org ? `${getAPIUrl()}usergroups/${props.usergroup_id}/users` : null,
swrFetcher (url) => swrFetcher(url, access_token)
) )
const isUserPartOfGroup = (user_id: any) => { const isUserPartOfGroup = (user_id: any) => {

View file

@ -6,7 +6,7 @@ import { getUriWithOrg } from '@services/config/config'
import { deleteCourseFromBackend } from '@services/courses/courses' import { deleteCourseFromBackend } from '@services/courses/courses'
import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { revalidateTags } from '@services/utils/ts/requests' import { revalidateTags } from '@services/utils/ts/requests'
import { Settings, X } from 'lucide-react' import { BookMinus, FilePenLine, Settings, Settings2, X, EllipsisVertical } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
@ -71,7 +71,10 @@ function CourseThumbnail(props: PropsType) {
/> />
)} )}
</Link> </Link>
<h2 className="font-bold text-lg w-[250px] py-2">{props.course.name}</h2> <div className='flex flex-col w-[250px] pt-3 space-y-2'>
<h2 className="font-bold text-gray-800 max-h-[80px] h-fit line-clamp-2 leading-tight text-lg capitalize">{props.course.name}</h2>
<h3 className='text-sm text-gray-700 leading-normal line-clamp-3'>{props.course.description}</h3>
</div>
</div> </div>
) )
} }
@ -89,7 +92,24 @@ const AdminEditsArea = (props: {
checkMethod="roles" checkMethod="roles"
orgId={props.course.org_id} orgId={props.course.org_id}
> >
<div className="flex space-x-2 absolute z-20 bottom-14 right-[15px] transform"> <div
className="flex items-center space-x-2 absolute z-20 overflow-hidden rounded-xl pt-0 mx-auto justify-center transform w-full h-[60px] bg-gradient-to-t from-transparent from-10% to-gray-900/60">
<Link
href={getUriWithOrg(
props.orgSlug,
'/dash/courses/course/' +
removeCoursePrefix(props.courseId) +
'/content'
)}
prefetch
>
<div
className="hover:cursor-pointer p-1 px-4 bg-blue-600 rounded-xl items-center flex shadow-2xl"
rel="noopener noreferrer"
>
<FilePenLine size={14} className="text-blue-200 font-bold" />
</div>
</Link>
<Link <Link
href={getUriWithOrg( href={getUriWithOrg(
props.orgSlug, props.orgSlug,
@ -97,24 +117,26 @@ const AdminEditsArea = (props: {
removeCoursePrefix(props.courseId) + removeCoursePrefix(props.courseId) +
'/general' '/general'
)} )}
prefetch
> >
<div <div
className=" hover:cursor-pointer p-1 px-4 bg-slate-700 rounded-xl items-center flex shadow-xl" className=" hover:cursor-pointer p-1 px-4 bg-gray-800 rounded-xl items-center flex shadow-2xl"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Settings size={14} className="text-slate-200 font-bold" /> <Settings2 size={14} className="text-gray-200 font-bold" />
</div> </div>
</Link> </Link>
<EllipsisVertical size={14} className='text-gray-200 font-bold' />
<ConfirmationModal <ConfirmationModal
confirmationButtonText="Delete Course" confirmationButtonText="Delete Course"
confirmationMessage="Are you sure you want to delete this course?" confirmationMessage="Are you sure you want to delete this course?"
dialogTitle={'Delete ' + props.course.name + ' ?'} dialogTitle={'Delete ' + props.course.name + ' ?'}
dialogTrigger={ dialogTrigger={
<div <div
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-xl items-center justify-center flex shadow-xl" className="hover:cursor-pointer p-1 px-4 bg-rose-600 h-fit rounded-xl items-center flex shadow-2xl"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<X size={14} className="text-rose-200 font-bold" /> <BookMinus size={14} className="text-rose-200 font-bold" />
</div> </div>
} }
functionToExecute={() => props.deleteCourses(props.courseId)} functionToExecute={() => props.deleteCourses(props.courseId)}

View file

@ -36,7 +36,7 @@ export const HeaderProfileBox = () => {
<AccountArea className="space-x-0"> <AccountArea className="space-x-0">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className='flex items-center space-x-2' > <div className='flex items-center space-x-2' >
<p className='text-sm'>{session.data.user.username}</p> <p className='text-sm capitalize'>{session.data.user.username}</p>
{isUserAdmin.isAdmin && <div className="text-[10px] bg-rose-300 px-2 font-bold rounded-md shadow-inner py-1">ADMIN</div>} {isUserAdmin.isAdmin && <div className="text-[10px] bg-rose-300 px-2 font-bold rounded-md shadow-inner py-1">ADMIN</div>}
</div> </div>
<div className="py-4"> <div className="py-4">

View file

@ -39,8 +39,9 @@
"katex": "^0.16.10", "katex": "^0.16.10",
"lowlight": "^3.1.0", "lowlight": "^3.1.0",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"next": "14.2.3", "next": "14.2.4",
"next-auth": "^4.24.7", "next-auth": "^4.24.7",
"nextjs-toploader": "^1.6.12",
"prosemirror-state": "^1.4.3", "prosemirror-state": "^1.4.3",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"re-resizable": "^6.9.17", "re-resizable": "^6.9.17",
@ -59,7 +60,7 @@
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.6", "y-prosemirror": "^1.2.8",
"y-webrtc": "^10.3.0", "y-webrtc": "^10.3.0",
"yjs": "^13.6.16" "yjs": "^13.6.16"
}, },

147
apps/web/pnpm-lock.yaml generated
View file

@ -43,10 +43,10 @@ importers:
version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/extension-code-block@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0) version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/extension-code-block@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0)
'@tiptap/extension-collaboration': '@tiptap/extension-collaboration':
specifier: ^2.4.0 specifier: ^2.4.0
version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0)(y-prosemirror@1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16)) version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0)(y-prosemirror@1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16))
'@tiptap/extension-collaboration-cursor': '@tiptap/extension-collaboration-cursor':
specifier: ^2.4.0 specifier: ^2.4.0
version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(y-prosemirror@1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16)) version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(y-prosemirror@1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16))
'@tiptap/extension-youtube': '@tiptap/extension-youtube':
specifier: ^2.4.0 specifier: ^2.4.0
version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0)) version: 2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))
@ -93,11 +93,14 @@ importers:
specifier: ^0.363.0 specifier: ^0.363.0
version: 0.363.0(react@18.3.1) version: 0.363.0(react@18.3.1)
next: next:
specifier: 14.2.3 specifier: 14.2.4
version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-auth: next-auth:
specifier: ^4.24.7 specifier: ^4.24.7
version: 4.24.7(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 4.24.7(next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nextjs-toploader:
specifier: ^1.6.12
version: 1.6.12(next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
prosemirror-state: prosemirror-state:
specifier: ^1.4.3 specifier: ^1.4.3
version: 1.4.3 version: 1.4.3
@ -153,8 +156,8 @@ importers:
specifier: ^9.0.12 specifier: ^9.0.12
version: 9.0.12(yjs@13.6.16) version: 9.0.12(yjs@13.6.16)
y-prosemirror: y-prosemirror:
specifier: ^1.2.6 specifier: ^1.2.8
version: 1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16) version: 1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16)
y-webrtc: y-webrtc:
specifier: ^10.3.0 specifier: ^10.3.0
version: 10.3.0(yjs@13.6.16) version: 10.3.0(yjs@13.6.16)
@ -427,62 +430,62 @@ packages:
'@lifeomic/attempt@3.1.0': '@lifeomic/attempt@3.1.0':
resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==} resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==}
'@next/env@14.2.3': '@next/env@14.2.4':
resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} resolution: {integrity: sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==}
'@next/eslint-plugin-next@14.2.3': '@next/eslint-plugin-next@14.2.3':
resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==} resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==}
'@next/swc-darwin-arm64@14.2.3': '@next/swc-darwin-arm64@14.2.4':
resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} resolution: {integrity: sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/swc-darwin-x64@14.2.3': '@next/swc-darwin-x64@14.2.4':
resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} resolution: {integrity: sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/swc-linux-arm64-gnu@14.2.3': '@next/swc-linux-arm64-gnu@14.2.4':
resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} resolution: {integrity: sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-arm64-musl@14.2.3': '@next/swc-linux-arm64-musl@14.2.4':
resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} resolution: {integrity: sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-x64-gnu@14.2.3': '@next/swc-linux-x64-gnu@14.2.4':
resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} resolution: {integrity: sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-linux-x64-musl@14.2.3': '@next/swc-linux-x64-musl@14.2.4':
resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} resolution: {integrity: sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-win32-arm64-msvc@14.2.3': '@next/swc-win32-arm64-msvc@14.2.4':
resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} resolution: {integrity: sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/swc-win32-ia32-msvc@14.2.3': '@next/swc-win32-ia32-msvc@14.2.4':
resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} resolution: {integrity: sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@next/swc-win32-x64-msvc@14.2.3': '@next/swc-win32-x64-msvc@14.2.4':
resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} resolution: {integrity: sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -2082,8 +2085,8 @@ packages:
nodemailer: nodemailer:
optional: true optional: true
next@14.2.3: next@14.2.4:
resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} resolution: {integrity: sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==}
engines: {node: '>=18.17.0'} engines: {node: '>=18.17.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -2100,6 +2103,13 @@ packages:
sass: sass:
optional: true optional: true
nextjs-toploader@1.6.12:
resolution: {integrity: sha512-nbun5lvVjlKnxLQlahzZ55nELVEduqoEXT03KCHnsEYJnFpI/3BaIzpMyq/v8C7UGU2NfxQmjq6ldZ310rsDqA==}
peerDependencies:
next: '>= 6.0.0'
react: '>= 16.0.0'
react-dom: '>= 16.0.0'
node-releases@2.0.14: node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
@ -2111,6 +2121,9 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
nprogress@0.2.0:
resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
oauth@0.9.15: oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
@ -2891,8 +2904,8 @@ packages:
peerDependencies: peerDependencies:
yjs: ^13.0.0 yjs: ^13.0.0
y-prosemirror@1.2.6: y-prosemirror@1.2.8:
resolution: {integrity: sha512-rGz8kX4v/uFJrLaqZvsezY1JGN/zTDSPMO76zRbNcpE63OEiw2PBCEQi9ZlfbEwgCMoeJLUT+otNyO/Oj73TGQ==} resolution: {integrity: sha512-xNDOEe9ViBXck0qwcTvzGgj832ecoz8GQSppoh6PwUokbXoEBDbAH76Qs15HOiatjZkSODHRGdpYlLBBkJPiGA==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'} engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies: peerDependencies:
prosemirror-model: ^1.7.1 prosemirror-model: ^1.7.1
@ -3137,37 +3150,37 @@ snapshots:
'@lifeomic/attempt@3.1.0': {} '@lifeomic/attempt@3.1.0': {}
'@next/env@14.2.3': {} '@next/env@14.2.4': {}
'@next/eslint-plugin-next@14.2.3': '@next/eslint-plugin-next@14.2.3':
dependencies: dependencies:
glob: 10.3.10 glob: 10.3.10
'@next/swc-darwin-arm64@14.2.3': '@next/swc-darwin-arm64@14.2.4':
optional: true optional: true
'@next/swc-darwin-x64@14.2.3': '@next/swc-darwin-x64@14.2.4':
optional: true optional: true
'@next/swc-linux-arm64-gnu@14.2.3': '@next/swc-linux-arm64-gnu@14.2.4':
optional: true optional: true
'@next/swc-linux-arm64-musl@14.2.3': '@next/swc-linux-arm64-musl@14.2.4':
optional: true optional: true
'@next/swc-linux-x64-gnu@14.2.3': '@next/swc-linux-x64-gnu@14.2.4':
optional: true optional: true
'@next/swc-linux-x64-musl@14.2.3': '@next/swc-linux-x64-musl@14.2.4':
optional: true optional: true
'@next/swc-win32-arm64-msvc@14.2.3': '@next/swc-win32-arm64-msvc@14.2.4':
optional: true optional: true
'@next/swc-win32-ia32-msvc@14.2.3': '@next/swc-win32-ia32-msvc@14.2.4':
optional: true optional: true
'@next/swc-win32-x64-msvc@14.2.3': '@next/swc-win32-x64-msvc@14.2.4':
optional: true optional: true
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
@ -3536,16 +3549,16 @@ snapshots:
dependencies: dependencies:
'@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0)
'@tiptap/extension-collaboration-cursor@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(y-prosemirror@1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16))': '@tiptap/extension-collaboration-cursor@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(y-prosemirror@1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16))':
dependencies: dependencies:
'@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0)
y-prosemirror: 1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16) y-prosemirror: 1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16)
'@tiptap/extension-collaboration@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0)(y-prosemirror@1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16))': '@tiptap/extension-collaboration@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))(@tiptap/pm@2.4.0)(y-prosemirror@1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16))':
dependencies: dependencies:
'@tiptap/core': 2.4.0(@tiptap/pm@2.4.0) '@tiptap/core': 2.4.0(@tiptap/pm@2.4.0)
'@tiptap/pm': 2.4.0 '@tiptap/pm': 2.4.0
y-prosemirror: 1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16) y-prosemirror: 1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16)
'@tiptap/extension-document@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))': '@tiptap/extension-document@2.4.0(@tiptap/core@2.4.0(@tiptap/pm@2.4.0))':
dependencies: dependencies:
@ -4237,7 +4250,7 @@ snapshots:
eslint: 8.57.0 eslint: 8.57.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.34.2(eslint@8.57.0) eslint-plugin-react: 7.34.2(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
@ -4261,7 +4274,7 @@ snapshots:
enhanced-resolve: 5.17.0 enhanced-resolve: 5.17.0
eslint: 8.57.0 eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2 fast-glob: 3.3.2
get-tsconfig: 4.7.5 get-tsconfig: 4.7.5
is-core-module: 2.13.1 is-core-module: 2.13.1
@ -4283,7 +4296,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies: dependencies:
array-includes: 3.1.8 array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5 array.prototype.findlastindex: 1.2.5
@ -4928,13 +4941,13 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
next-auth@4.24.7(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): next-auth@4.24.7(next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.24.7 '@babel/runtime': 7.24.7
'@panva/hkdf': 1.1.1 '@panva/hkdf': 1.1.1
cookie: 0.5.0 cookie: 0.5.0
jose: 4.15.5 jose: 4.15.5
next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: 14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
oauth: 0.9.15 oauth: 0.9.15
openid-client: 5.6.5 openid-client: 5.6.5
preact: 10.22.0 preact: 10.22.0
@ -4943,9 +4956,9 @@ snapshots:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
uuid: 8.3.2 uuid: 8.3.2
next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@next/env': 14.2.3 '@next/env': 14.2.4
'@swc/helpers': 0.5.5 '@swc/helpers': 0.5.5
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001632 caniuse-lite: 1.0.30001632
@ -4955,25 +4968,35 @@ snapshots:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.1(react@18.3.1) styled-jsx: 5.1.1(react@18.3.1)
optionalDependencies: optionalDependencies:
'@next/swc-darwin-arm64': 14.2.3 '@next/swc-darwin-arm64': 14.2.4
'@next/swc-darwin-x64': 14.2.3 '@next/swc-darwin-x64': 14.2.4
'@next/swc-linux-arm64-gnu': 14.2.3 '@next/swc-linux-arm64-gnu': 14.2.4
'@next/swc-linux-arm64-musl': 14.2.3 '@next/swc-linux-arm64-musl': 14.2.4
'@next/swc-linux-x64-gnu': 14.2.3 '@next/swc-linux-x64-gnu': 14.2.4
'@next/swc-linux-x64-musl': 14.2.3 '@next/swc-linux-x64-musl': 14.2.4
'@next/swc-win32-arm64-msvc': 14.2.3 '@next/swc-win32-arm64-msvc': 14.2.4
'@next/swc-win32-ia32-msvc': 14.2.3 '@next/swc-win32-ia32-msvc': 14.2.4
'@next/swc-win32-x64-msvc': 14.2.3 '@next/swc-win32-x64-msvc': 14.2.4
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
nextjs-toploader@1.6.12(next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
next: 14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nprogress: 0.2.0
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
node-releases@2.0.14: {} node-releases@2.0.14: {}
normalize-path@3.0.0: {} normalize-path@3.0.0: {}
normalize-range@0.1.2: {} normalize-range@0.1.2: {}
nprogress@0.2.0: {}
oauth@0.9.15: {} oauth@0.9.15: {}
object-assign@4.1.1: {} object-assign@4.1.1: {}
@ -5880,7 +5903,7 @@ snapshots:
lib0: 0.2.94 lib0: 0.2.94
yjs: 13.6.16 yjs: 13.6.16
y-prosemirror@1.2.6(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16): y-prosemirror@1.2.8(prosemirror-model@1.21.1)(prosemirror-state@1.4.3)(prosemirror-view@1.33.7)(y-protocols@1.0.6(yjs@13.6.16))(yjs@13.6.16):
dependencies: dependencies:
lib0: 0.2.94 lib0: 0.2.94
prosemirror-model: 1.21.1 prosemirror-model: 1.21.1

View file

@ -51,3 +51,5 @@ export const getDefaultOrg = () => {
export const getCollaborationServerUrl = () => { export const getCollaborationServerUrl = () => {
return `${LEARNHOUSE_COLLABORATION_WS_URL}` return `${LEARNHOUSE_COLLABORATION_WS_URL}`
} }