diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index b960e2c1..fd34d842 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -195,16 +195,16 @@ async def delete_chapter( # RBAC check await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session) - db_session.delete(chapter) - db_session.commit() - - # Remove all linked activities - statement = select(ChapterActivity).where(ChapterActivity.id == chapter.id) + # Remove all linked chapter activities + statement = select(ChapterActivity).where(ChapterActivity.chapter_id == chapter.id) chapter_activities = db_session.exec(statement).all() for chapter_activity in chapter_activities: db_session.delete(chapter_activity) - db_session.commit() + + # Delete the chapter + db_session.delete(chapter) + db_session.commit() return {"detail": "chapter deleted"} diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx index c09d9dce..343bfacf 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx @@ -74,87 +74,84 @@ const CollectionsPage = async (params: any) => { return ( -
- - - +
+ + - - - -
-
- {collections.map((collection: any) => ( -
- -
- ))} - {collections.length == 0 && ( -
-
-
- - - - -
-
-

+ + + + +

+
+ {collections.map((collection: any) => ( +
+ +
+ ))} + {collections.length === 0 && ( +
+
+
+ + + + +
+

No collections yet

-

+

+
+ + + + + +
- - - - -
-
- )} + )} +
) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 91e0aac7..3dc1853b 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -25,6 +25,7 @@ import AssignmentSubmissionProvider, { useAssignmentSubmission } from '@compone import toast from 'react-hot-toast' import { mutate } from 'swr' import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal' +import { useMediaQuery } from 'usehooks-ts' interface ActivityClientProps { activityid: string @@ -47,6 +48,7 @@ function ActivityClient(props: ActivityClientProps) { const [bgColor, setBgColor] = React.useState('bg-white') const [assignment, setAssignment] = React.useState(null) as any; const [markStatusButtonActive, setMarkStatusButtonActive] = React.useState(false); + function getChapterNameByActivityId(course: any, activity_id: any) { for (let i = 0; i < course.chapters.length; i++) { @@ -223,7 +225,7 @@ export function MarkStatus(props: { }) { const router = useRouter() const session = useLHSession() as any; - + const isMobile = useMediaQuery('(max-width: 768px)') async function markActivityAsCompleteFront() { const trail = await markActivityAsComplete( props.orgslug, @@ -263,7 +265,7 @@ export function MarkStatus(props: { {' '} - Mark as complete + {!isMobile && Mark as complete}
)} diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx index fbf095de..b3311d8c 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx @@ -18,6 +18,7 @@ import UserAvatar from '@components/Objects/UserAvatar' import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates' import { CourseProvider } from '@components/Contexts/CourseContext' import { useLHSession } from '@components/Contexts/LHSessionContext' +import { useMediaQuery } from 'usehooks-ts' const CourseClient = (props: any) => { const [user, setUser] = useState({}) @@ -28,6 +29,7 @@ const CourseClient = (props: any) => { const course = props.course const org = useOrg() as any const router = useRouter() + const isMobile = useMediaQuery('(max-width: 768px)') function getLearningTags() { // create array of learnings from a string object (comma separated) @@ -72,21 +74,21 @@ const CourseClient = (props: any) => { ) : ( -
+

Course

-

{course.name}

+

{course.name}

-
- +
+ {!isMobile && - + }
{props.course?.thumbnail_image && org ? (
{ course={course} /> -
+

Description

@@ -141,7 +143,7 @@ const CourseClient = (props: any) => {
)} -

Course Lessons

+

Course Lessons

{course.chapters.map((chapter: any) => { return ( @@ -303,20 +305,20 @@ const CourseClient = (props: any) => { })}
-
+
{user && ( -
+
-
+
Author
-
+
{course.authors[0].first_name && course.authors[0].last_name && (
@@ -344,14 +346,14 @@ const CourseClient = (props: any) => { {isCourseStarted() ? ( ) : ( - } - /> - -
+
+
+ + + + } + dialogTitle="Create Course" + dialogDescription="Create a new course" + dialogTrigger={ + + } + /> + +
-
- {courses.map((course: any) => ( -
- -
- ))} - {courses.length == 0 && ( -
-
-
- - - - -
-
-

+
+ {courses.map((course: any) => ( +
+ +
+ ))} + {courses.length === 0 && ( +
+
+
+ + {/* ... SVG content ... */} + +
+

No courses yet

- {isUserAdmin ? (

- Create a course to add content -

) : (

- No courses available yet -

)} +

+ {isUserAdmin ? ( + "Create a course to add content" + ) : ( + "No courses available yet" + )} +

+ {isUserAdmin && ( +
+ + + } + dialogTitle="Create Course" + dialogDescription="Create a new course" + dialogTrigger={ + + } + /> + +
+ )}
- - - } - dialogTitle="Create Course" - dialogDescription="Create a new course" - dialogTrigger={ - - } - /> -
-
- )} + )} +

diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx index 32c5df3b..004ce80b 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx @@ -82,48 +82,44 @@ const OrgHomePage = async (params: any) => { ) return ( -
+
{/* Collections */} -
-
+
+
-
- - - - - -
-
- {collections.map((collection: any) => ( -
- -
- ))} - {collections.length == 0 && ( -
-
-
-
+ + + + +
+
+ {collections.map((collection: any) => ( +
+ +
+ ))} + {collections.length === 0 && ( +
+
+
{ />
-
-

- No collections yet -

-

- -

-
+

+ No collections yet +

+

+ +

-
- )} + )} +
{/* Courses */} -
-
-
+
+
+ + + + +
- - - - - -
-
- {courses.map((course: any) => ( -
- -
- ))} - {courses.length == 0 && ( -
-
-
-
+
+ {courses.map((course: any) => ( +
+ +
+ ))} + {courses.length === 0 && ( +
+
+
{ />
-
-

- No courses yet -

-

- -

-
+

+ No courses yet +

+

+ +

-
- )} + )} +
diff --git a/apps/web/app/orgs/[orgslug]/dash/ClientAdminLayout.tsx b/apps/web/app/orgs/[orgslug]/dash/ClientAdminLayout.tsx index 32778e18..48370ca3 100644 --- a/apps/web/app/orgs/[orgslug]/dash/ClientAdminLayout.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/ClientAdminLayout.tsx @@ -1,8 +1,10 @@ 'use client'; -import LeftMenu from '@components/Dashboard/UI/LeftMenu' +import DashLeftMenu from '@components/Dashboard/UI/DashLeftMenu' +import DashMobileMenu from '@components/Dashboard/UI/DashMobileMenu' import AdminAuthorization from '@components/Security/AdminAuthorization' import { SessionProvider } from 'next-auth/react' -import React from 'react' +import React, { useState, useEffect } from 'react' +import { useMediaQuery } from 'usehooks-ts'; function ClientAdminLayout({ children, @@ -11,11 +13,17 @@ function ClientAdminLayout({ children: React.ReactNode params: any }) { + const isMobile = useMediaQuery('(max-width: 768px)') + return ( -
- +
+ {isMobile ? ( + + ) : ( + + )}
{children}
@@ -23,4 +31,4 @@ function ClientAdminLayout({ ) } -export default ClientAdminLayout \ No newline at end of file +export default ClientAdminLayout diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx index 1027ed60..7c3144a5 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx @@ -1,6 +1,6 @@ 'use client'; import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' -import { BookOpen, BookX, EllipsisVertical, Eye, Layers2, UserRoundPen } from 'lucide-react' +import { BookOpen, BookX, EllipsisVertical, Eye, Layers2, Monitor, UserRoundPen } from 'lucide-react' import React, { useEffect } from 'react' import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; import ToolTip from '@components/StyledElements/Tooltip/Tooltip'; @@ -15,12 +15,29 @@ import { updateActivity } from '@services/courses/activities'; // Lazy Loading import dynamic from 'next/dynamic'; import AssignmentEditorSubPage from './subpages/AssignmentEditorSubPage'; +import { useMediaQuery } from 'usehooks-ts'; const AssignmentSubmissionsSubPage = dynamic(() => import('./subpages/AssignmentSubmissionsSubPage')) function AssignmentEdit() { const params = useParams<{ assignmentuuid: string; }>() const searchParams = useSearchParams() const [selectedSubPage, setSelectedSubPage] = React.useState(searchParams.get('subpage') || 'editor') + const isMobile = useMediaQuery('(max-width: 767px)') + + if (isMobile) { + // TODO: Work on a better mobile experience + return ( +
+
+

Desktop Only

+ +

This page is only accessible from a desktop device.

+

Please switch to a desktop to view and manage the assignment.

+
+
+ ) + } + return (
diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx index ad70f984..8e11e60b 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx @@ -46,19 +46,19 @@ function AssignmentsHome() { return (
-
+

Assignments

{courseAssignments.map((assignments: any, index: number) => ( -
+
-
+
-
+

Course

{courses[index].name}

@@ -75,10 +75,9 @@ function AssignmentsHome() {
- {assignments && assignments.map((assignment: any) => ( -
-
+
+

Assignment

@@ -86,7 +85,6 @@ function AssignmentsHome() {
{assignment.description}
- @@ -124,10 +121,8 @@ function AssignmentsHome() {
))} -
-
) } @@ -172,4 +167,4 @@ const MiniThumbnail = (props: { course: any }) => { } -export default AssignmentsHome \ No newline at end of file +export default AssignmentsHome diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx index 31ae5ce3..57f1ba2a 100644 --- a/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx @@ -1,12 +1,13 @@ 'use client' import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse' -import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail' +import CourseThumbnail, { removeCoursePrefix } from '@components/Objects/Thumbnails/CourseThumbnail' import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement' import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton' import Modal from '@components/StyledElements/Modal/Modal' import { useSearchParams } from 'next/navigation' import React from 'react' +import useAdminStatus from '@components/Hooks/useAdminStatus' type CourseProps = { orgslug: string @@ -20,114 +21,106 @@ function CoursesHome(params: CourseProps) { const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse) const orgslug = params.orgslug const courses = params.courses + const isUserAdmin = useAdminStatus() as any async function closeNewCourseModal() { setNewCourseModal(false) } return ( -
-
-
- -
-
Courses
- - - } - dialogTitle="Create Course" - dialogDescription="Create a new course" - dialogTrigger={ - - } - /> - -
+
+
+ +
+

Courses

+ + + } + dialogTitle="Create Course" + dialogDescription="Create a new course" + dialogTrigger={ + + } + /> +
-
+ +
{courses.map((course: any) => ( -
- +
+
))} - {courses.length == 0 && ( -
-
-
+ {courses.length === 0 && ( +
+
+
- - + {/* ... SVG content ... */}
-
-

- No courses yet -

-

- Create a course to add content -

-
- - - } - dialogTitle="Create Course" - dialogDescription="Create a new course" - dialogTrigger={ - - } - /> - +

+ No courses yet +

+

+ {isUserAdmin ? ( + "Create a course to add content" + ) : ( + "No courses available yet" + )} +

+ {isUserAdmin && ( +
+ + + } + dialogTitle="Create Course" + dialogDescription="Create a new course" + dialogTrigger={ + + } + /> + +
+ )}
)} diff --git a/apps/web/app/orgs/[orgslug]/dash/page.tsx b/apps/web/app/orgs/[orgslug]/dash/page.tsx index acc01033..a356a5fb 100644 --- a/apps/web/app/orgs/[orgslug]/dash/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/page.tsx @@ -7,84 +7,68 @@ import AdminAuthorization from '@components/Security/AdminAuthorization' function DashboardHome() { return ( -
-
+
+
learnhouse logo + className="w-48 sm:w-auto" + />
-
- -
- -
Courses
-

- Create and manage courses, chapters and ativities{' '} -

-
- - -
- -
- Organization -
-

- Configure your Organization general settings{' '} -

-
- - -
- -
Users
-

- Manage your Organization's users, roles{' '} -

-
- +
+ {/* Card components */} + } + title="Courses" + description="Create and manage courses, chapters and activities" + /> + } + title="Organization" + description="Configure your Organization general settings" + /> + } + title="Users" + description="Manage your Organization's users, roles" + />
-
+
- -
+ +
LearnHouse University
-
+
-
- -
Account Settings
-

- Configure your personal settings, passwords, email -

+
+ +
+
Account Settings
+

+ Configure your personal settings, passwords, email +

+
@@ -92,4 +76,20 @@ function DashboardHome() { ) } +// New component for dashboard cards +function DashboardCard({ href, icon, title, description }: { href: string, icon: React.ReactNode, title: string, description: string }) { + return ( + +
+ {icon} +
{title}
+

{description}

+
+ + ) +} + export default DashboardHome diff --git a/apps/web/app/orgs/[orgslug]/dash/users/settings/[subpage]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/users/settings/[subpage]/page.tsx index 995c3a68..d6342e38 100644 --- a/apps/web/app/orgs/[orgslug]/dash/users/settings/[subpage]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/users/settings/[subpage]/page.tsx @@ -2,8 +2,9 @@ import React, { useEffect } from 'react' import { motion } from 'framer-motion' import Link from 'next/link' +import { useMediaQuery } from 'usehooks-ts' import { getUriWithOrg } from '@services/config/config' -import { ScanEye, SquareUserRound, UserPlus, Users } from 'lucide-react' +import { Monitor, ScanEye, SquareUserRound, UserPlus, Users } from 'lucide-react' import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' import { useLHSession } from '@components/Contexts/LHSessionContext' import { useOrg } from '@components/Contexts/OrgContext' @@ -22,6 +23,7 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) { const org = useOrg() as any const [H1Label, setH1Label] = React.useState('') const [H2Label, setH2Label] = React.useState('') + const isMobile = useMediaQuery('(max-width: 767px)') function handleLabels() { if (params.subpage == 'users') { @@ -46,6 +48,20 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) { handleLabels() }, [session, org, params.subpage, params]) + if (isMobile) { + // TODO: Work on a better mobile experience + return ( +
+
+

Desktop Only

+ +

This page is only accessible from a desktop device.

+

Please switch to a desktop to view and manage user settings.

+
+
+ ) + } + return (
diff --git a/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx b/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx index eac3c8db..a682bef5 100644 --- a/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx +++ b/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx @@ -50,14 +50,14 @@ function EditCourseAccess(props: EditCourseAccessProps) { {courseStructure && (
-
-
-

Access to the course

-

+
+
+

Access to the course

+

Choose if you want your course to be publicly available on the internet or only accessible to signed in users

-
+
)} -
- -
+
+ +
Public
-
+
The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone
@@ -94,12 +94,12 @@ function EditCourseAccess(props: EditCourseAccessProps) { Active
)} -
- -
+
+ +
Users Only
-
+
The Course is only accessible to signed in users, additionally you can choose which UserGroups can access this course
@@ -139,42 +139,44 @@ function UserGroupsSection({ usergroups }: { usergroups: any[] }) { return ( <> -
-

UserGroups

-

+
+

UserGroups

+

You can choose to give access to this course to specific groups of users only by linking it to a UserGroup

- - - - - - - - - {usergroups?.map((usergroup: any) => ( - - - +
+
NameActions
{usergroup.name} - - - Delete link - - } - functionToExecute={() => removeUserGroupLink(usergroup.id)} - status="warning" - /> -
+ + + + - ))} - -
NameActions
+ + + {usergroups?.map((usergroup: any) => ( + + {usergroup.name} + + + + Delete link + + } + functionToExecute={() => removeUserGroupLink(usergroup.id)} + status="warning" + /> + + + ))} + + +

- + } diff --git a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx index 54624707..642f9162 100644 --- a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx +++ b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx @@ -26,6 +26,7 @@ import { deleteAssignmentUsingActivityUUID, getAssignmentFromActivityUUID } from import { useOrg } from '@components/Contexts/OrgContext' import { useCourse } from '@components/Contexts/CourseContext' import toast from 'react-hot-toast' +import { useMediaQuery } from 'usehooks-ts' type ActivitiyElementProps = { orgslug: string @@ -50,6 +51,7 @@ function ActivityElement(props: ActivitiyElementProps) { string | undefined >(undefined) const activityUUID = props.activity.activity_uuid + const isMobile = useMediaQuery('(max-width: 767px)') async function deleteActivityUI() { const toast_loading = toast.loading('Deleting activity...') @@ -110,14 +112,14 @@ function ActivityElement(props: ActivitiyElementProps) { > {(provided, snapshot) => (
{/* Activity Type Icon */} - + {/* Centered Activity Name */}
@@ -143,13 +145,11 @@ function ActivityElement(props: ActivitiyElementProps) { onClick={() => updateActivityName(props.activity.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900" > - +
) : ( -

{props.activity.name}

+

{props.activity.name}

)} setSelectedActivity(props.activity.id)} @@ -157,65 +157,60 @@ function ActivityElement(props: ActivitiyElementProps) { />
- - {/* Edit and View Button */} -
-
- - {/* Publishing */} -
changePublicStatus()} - > - {!props.activity.published ? ( - - ) : ( - - )} - {!props.activity.published ? 'Publish' : 'Unpublish'} -
- - - Preview - -
+ {/* Edit, View, Publish, and Delete Buttons */} +
+ + {/* Publishing */} + + + + Preview + {/* Delete Button */} -
- - - -
- } - functionToExecute={() => deleteActivityUI()} - status="warning" - > -
+ + + {!isMobile && Delete} + + } + functionToExecute={() => deleteActivityUI()} + status="warning" + />
)} @@ -242,11 +237,11 @@ const ACTIVITIES = { } } -const ActivityTypeIndicator = ({activityType} : { activityType: keyof typeof ACTIVITIES}) => { +const ActivityTypeIndicator = ({activityType, isMobile} : { activityType: keyof typeof ACTIVITIES, isMobile: boolean}) => { const {displayName, Icon} = ACTIVITIES[activityType] return ( -
+
{' '}
@@ -257,7 +252,7 @@ const ActivityTypeIndicator = ({activityType} : { activityType: keyof typeof ACT ) } -const ActivityElementOptions = ({ activity }: any) => { +const ActivityElementOptions = ({ activity, isMobile }: { activity: any; isMobile: boolean }) => { const [assignmentUUID, setAssignmentUUID] = useState(''); const org = useOrg() as any; const course = useCourse() as any; @@ -299,11 +294,11 @@ const ActivityElementOptions = ({ activity }: any) => { )}/edit` } prefetch - className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center" - target='_blank' // hotfix for an editor prosemirror bug + className={`hover:cursor-pointer p-1 ${isMobile ? 'px-2' : 'px-3'} bg-sky-700 rounded-md items-center`} + target='_blank' >
- Edit Page + Edit Page
@@ -316,10 +311,10 @@ const ActivityElementOptions = ({ activity }: any) => { `/dash/assignments/${assignmentUUID}` } prefetch - className=" hover:cursor-pointer p-1 px-3 bg-teal-700 rounded-md items-center" + className={`hover:cursor-pointer p-1 ${isMobile ? 'px-2' : 'px-3'} bg-teal-700 rounded-md items-center`} >
- Edit Assignment + {!isMobile && Edit Assignment}
diff --git a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx index 57aab3ec..b3d27980 100644 --- a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx +++ b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx @@ -6,6 +6,7 @@ import { Pencil, Save, X, + Trash2, } from 'lucide-react' import React from 'react' import { Draggable, Droppable } from 'react-beautiful-dnd' @@ -71,27 +72,27 @@ function ChapterElement(props: ChapterElementProps) { > {(provided, snapshot) => (
-
-
+
+
-
+
{selectedChapter === props.chapter.id ? ( -
+
updateChapterName(props.chapter.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900" > - updateChapterName(props.chapter.id)} - /> +
) : ( -

+

{props.chapter.name}

)} @@ -127,23 +125,24 @@ function ChapterElement(props: ChapterElementProps) { />
- - - -

Delete Chapter

-
- } - functionToExecute={() => deleteChapterUI()} - status="warning" - > +
+ + + + + } + functionToExecute={() => deleteChapterUI()} + status="warning" + /> +
{}, [org]) return ( -
+
{({ isSubmitting }) => (
-
-
+
+
@@ -149,24 +149,24 @@ function OrgEditGeneral() {
-
- - +
+ + Logo Thumbnail
-
-
+
+
@@ -189,16 +189,16 @@ function OrgEditGeneral() {
-

Accepts PNG , JPG

+

Accepts PNG, JPG

-
-
+
+
diff --git a/apps/web/components/Dashboard/UI/LeftMenu.tsx b/apps/web/components/Dashboard/UI/DashLeftMenu.tsx similarity index 99% rename from apps/web/components/Dashboard/UI/LeftMenu.tsx rename to apps/web/components/Dashboard/UI/DashLeftMenu.tsx index bc2eff28..d3ab2e3c 100644 --- a/apps/web/components/Dashboard/UI/LeftMenu.tsx +++ b/apps/web/components/Dashboard/UI/DashLeftMenu.tsx @@ -12,7 +12,7 @@ import AdminAuthorization from '@components/Security/AdminAuthorization' import { useLHSession } from '@components/Contexts/LHSessionContext' import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' -function LeftMenu() { +function DashLeftMenu() { const org = useOrg() as any const session = useLHSession() as any const [loading, setLoading] = React.useState(true) @@ -176,4 +176,4 @@ function LeftMenu() { ) } -export default LeftMenu +export default DashLeftMenu diff --git a/apps/web/components/Dashboard/UI/DashMobileMenu.tsx b/apps/web/components/Dashboard/UI/DashMobileMenu.tsx new file mode 100644 index 00000000..231f758b --- /dev/null +++ b/apps/web/components/Dashboard/UI/DashMobileMenu.tsx @@ -0,0 +1,69 @@ +'use client' +import { useOrg } from '@components/Contexts/OrgContext' +import { signOut } from 'next-auth/react' +import { Backpack, BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react' +import Link from 'next/link' +import React from 'react' +import AdminAuthorization from '@components/Security/AdminAuthorization' +import { useLHSession } from '@components/Contexts/LHSessionContext' +import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' +import ToolTip from '@components/StyledElements/Tooltip/Tooltip' + +function DashMobileMenu() { + const org = useOrg() as any + const session = useLHSession() as any + + async function logOutUI() { + const res = await signOut({ redirect: true, callbackUrl: getUriWithoutOrg('/login?orgslug=' + org.slug) }) + if (res) { + getUriWithOrg(org.slug, '/') + } + } + + return ( +
+
+ + + + + Home + + + + + + Courses + + + + + + Assignments + + + + + + Users + + + + + + Org + + + + + + + Settings + + +
+
+ ) +} + +export default DashMobileMenu diff --git a/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx b/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx index 691783d4..d9ea3a74 100644 --- a/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx +++ b/apps/web/components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral.tsx @@ -40,7 +40,7 @@ function UserEditGeneral() { useEffect(() => { }, [session, session.data]) return ( -
+
{session.data.user && ( {({ isSubmitting }) => ( -
- - - - - - - - - - - - - - - - - - +
+ +
+ {[ + { label: 'Email', name: 'email', type: 'email' }, + { label: 'Username', name: 'username', type: 'text' }, + { label: 'First Name', name: 'first_name', type: 'text' }, + { label: 'Last Name', name: 'last_name', type: 'text' }, + { label: 'Bio', name: 'bio', type: 'text' }, + ].map((field) => ( +
+ + +
+ ))} +
-
- - {error && ( -
- -
- {error} +
+
+ + {error && ( +
+ + {error}
-
- )} - {success && ( -
- -
- {success} + )} + {success && ( +
+ + {success}
-
- )} -
-
-
+ )} +
+
{localAvatar ? ( )} -
- {isLoading ? ( -
- -
+ {isLoading ? ( +
Uploading
-
- ) : ( -
- - -
- )} + ) : ( + <> + + + + )} +
-
- +
+

Recommended size 100x100

diff --git a/apps/web/components/Objects/Editor/Editor.tsx b/apps/web/components/Objects/Editor/Editor.tsx index c255d637..594de491 100644 --- a/apps/web/components/Objects/Editor/Editor.tsx +++ b/apps/web/components/Objects/Editor/Editor.tsx @@ -21,7 +21,7 @@ import WarningCallout from './Extensions/Callout/Warning/WarningCallout' import ImageBlock from './Extensions/Image/ImageBlock' import Youtube from '@tiptap/extension-youtube' import VideoBlock from './Extensions/Video/VideoBlock' -import { Eye } from 'lucide-react' +import { ComputerIcon, Eye, Monitor } from 'lucide-react' import MathEquationBlock from './Extensions/MathEquation/MathEquationBlock' import PDFBlock from './Extensions/PDF/PDFBlock' import QuizBlock from './Extensions/Quiz/QuizBlock' @@ -51,6 +51,7 @@ import { getUriWithOrg } from '@services/config/config' import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects' import Badges from './Extensions/Badges/Badges' import Buttons from './Extensions/Buttons/Buttons' +import { useMediaQuery } from 'usehooks-ts' interface Editor { content: string @@ -170,6 +171,21 @@ function Editor(props: Editor) { content: props.isCollabEnabledOnThisOrg ? null : props.content, }) + const isMobile = useMediaQuery('(max-width: 767px)') + if (isMobile) { + // TODO: Work on a better editor mobile experience + return ( +
+
+

Desktop Only

+ +

The editor is only accessible from a desktop device.

+

Please switch to a desktop to view.

+
+
+ ) + } + return ( diff --git a/apps/web/components/Objects/Menu/Menu.tsx b/apps/web/components/Objects/Menu/Menu.tsx index 1b77a619..ec33ddd8 100644 --- a/apps/web/components/Objects/Menu/Menu.tsx +++ b/apps/web/components/Objects/Menu/Menu.tsx @@ -14,54 +14,73 @@ export const Menu = (props: any) => { const access_token = session?.data?.tokens?.access_token; const [feedbackModal, setFeedbackModal] = React.useState(false) const org = useOrg() as any; + const [isMenuOpen, setIsMenuOpen] = React.useState(false) function closeFeedbackModal() { setFeedbackModal(false) } + function toggleMenu() { + setIsMenuOpen(!isMenuOpen) + } + return ( <> -
- -
-
-
-
- -
- {org?.logo_image ? ( - Learnhouse - ) : ( - - )} -
- +
+
+
+
+
+ +
+ {org?.logo_image ? ( + Learnhouse + ) : ( + + )} +
+ +
+
+ +
-
+
+
+ +
+ +
+
+
+
+
+
-
- {/* } - dialogTitle="Feedback" - dialogDescription="An issue? A suggestion? a bug ? Let us know!" - dialogTrigger={ -
- -
- } - /> */} +
diff --git a/apps/web/components/Objects/Thumbnails/CollectionThumbnail.tsx b/apps/web/components/Objects/Thumbnails/CollectionThumbnail.tsx index df27f05a..d1f92fe4 100644 --- a/apps/web/components/Objects/Thumbnails/CollectionThumbnail.tsx +++ b/apps/web/components/Objects/Thumbnails/CollectionThumbnail.tsx @@ -25,43 +25,42 @@ const removeCollectionPrefix = (collectionid: string) => { function CollectionThumbnail(props: PropsType) { const org = useOrg() as any return ( -
-
-
- {props.collection.courses.slice(0, 2).map((course: any) => ( - <> - -
- - - ))} +
+
+
+
+ {props.collection.courses.slice(0, 3).map((course: any, index: number) => ( +
+ ))} +
+
+ + {props.collection.name} + + + {props.collection.courses.length} course{props.collection.courses.length !== 1 ? 's' : ''} + +
- -

- {props.collection.name} -

- { orgId={props.org_id} checkMethod="roles" > -
+
- -
+ + } functionToExecute={() => deleteCollectionUI(props.collection_uuid)} status="warning" diff --git a/apps/web/components/Objects/Thumbnails/CourseThumbnail.tsx b/apps/web/components/Objects/Thumbnails/CourseThumbnail.tsx index 5a4ada50..dbb2a85d 100644 --- a/apps/web/components/Objects/Thumbnails/CourseThumbnail.tsx +++ b/apps/web/components/Objects/Thumbnails/CourseThumbnail.tsx @@ -6,146 +6,125 @@ import { getUriWithOrg } from '@services/config/config' import { deleteCourseFromBackend } from '@services/courses/courses' import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { revalidateTags } from '@services/utils/ts/requests' -import { BookMinus, FilePenLine, Settings2, EllipsisVertical } from 'lucide-react' +import { BookMinus, FilePenLine, Settings2, MoreVertical } from 'lucide-react' import { useLHSession } from '@components/Contexts/LHSessionContext' import Link from 'next/link' import { useRouter } from 'next/navigation' -import React, { useEffect } from 'react' +import React from 'react' import toast from 'react-hot-toast' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +type Course = { + course_uuid: string + name: string + description: string + thumbnail_image: string + org_id: string +} type PropsType = { - course: any + course: Course orgslug: string + customLink?: string } -// function to remove "course_" from the course_uuid -function removeCoursePrefix(course_uuid: string) { - return course_uuid.replace('course_', '') -} +export const removeCoursePrefix = (course_uuid: string) => course_uuid.replace('course_', '') -function CourseThumbnail(props: PropsType) { - const router = useRouter() +function CourseThumbnail({ course, orgslug, customLink }: PropsType) { + const router = useRouter() const org = useOrg() as any - const session = useLHSession() as any; + const session = useLHSession() as any - async function deleteCourses(course_uuid: any) { - const toast_loading = toast.loading('Deleting course...') - await deleteCourseFromBackend(course_uuid, session.data?.tokens?.access_token) - toast.dismiss(toast_loading) - toast.success('Course deleted successfully') - await revalidateTags(['courses'], props.orgslug) - - router.refresh() + const deleteCourse = async () => { + const toastId = toast.loading('Deleting course...') + try { + await deleteCourseFromBackend(course.course_uuid, session.data?.tokens?.access_token) + await revalidateTags(['courses'], orgslug) + toast.success('Course deleted successfully') + router.refresh() + } catch (error) { + toast.error('Failed to delete course') + } finally { + toast.dismiss(toastId) + } } - useEffect(() => { }, [org]) + const thumbnailImage = course.thumbnail_image + ? getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image) + : '../empty_thumbnail.png' return (
- - - {props.course.thumbnail_image ? ( -
- ) : ( -
- )} + +
-
-

{props.course.name}

-

{props.course.description}

+
+

{course.name}

+

{course.description}

) } -const AdminEditsArea = (props: { +const AdminEditOptions = ({ course, orgSlug, deleteCourse }: { + course: Course orgSlug: string - courseId: string - course: any - deleteCourses: any + deleteCourse: () => Promise }) => { return ( -
- -
- -
- - -
- -
- - - - -
- } - functionToExecute={() => props.deleteCourses(props.courseId)} - status="warning" - > +
+ + + + + + + + Edit Content + + + + + Settings + + + + + Delete Course + + } + functionToExecute={deleteCourse} + status="warning" + /> + + +
) diff --git a/apps/web/components/StyledElements/Wrappers/GeneralWrapper.tsx b/apps/web/components/StyledElements/Wrappers/GeneralWrapper.tsx index 00dc7326..6e078b9b 100644 --- a/apps/web/components/StyledElements/Wrappers/GeneralWrapper.tsx +++ b/apps/web/components/StyledElements/Wrappers/GeneralWrapper.tsx @@ -1,9 +1,9 @@ function GeneralWrapperStyled({ children }: { children: React.ReactNode }) { return ( -
+
{children}
) } -export default GeneralWrapperStyled +export default GeneralWrapperStyled \ No newline at end of file diff --git a/apps/web/components/ui/dropdown-menu.tsx b/apps/web/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..76ad9152 --- /dev/null +++ b/apps/web/components/ui/dropdown-menu.tsx @@ -0,0 +1,205 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/apps/web/package.json b/apps/web/package.json index 10c0d6ee..bf2141ae 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --turbo", "dev-https": "next dev --experimental-https -p 443", "build": "next build", "start": "next start", @@ -17,6 +17,7 @@ "@radix-ui/colors": "^0.1.9", "@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-form": "^0.0.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-switch": "^1.1.1", @@ -69,6 +70,7 @@ "tailwind-scrollbar": "^3.1.0", "tailwindcss-animate": "^1.0.7", "unsplash-js": "^7.0.19", + "usehooks-ts": "^3.1.0", "uuid": "^9.0.1", "y-indexeddb": "^9.0.12", "y-prosemirror": "^1.2.12", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 9bd75d33..77e39efa 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-form': specifier: ^0.0.3 version: 0.0.3(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -182,6 +185,9 @@ importers: unsplash-js: specifier: ^7.0.19 version: 7.0.19 + usehooks-ts: + specifier: ^3.1.0 + version: 3.1.0(react@18.3.1) uuid: specifier: ^9.0.1 version: 9.0.1 @@ -963,6 +969,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.2': + resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -1034,6 +1053,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menu@2.1.2': + resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.0': resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} peerDependencies: @@ -2866,6 +2898,9 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3876,6 +3911,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + usehooks-ts@3.1.0: + resolution: {integrity: sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4788,6 +4829,21 @@ snapshots: '@types/react': 18.2.74 '@types/react-dom': 18.2.23 + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.74)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.74 + '@types/react-dom': 18.2.23 + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.2.74)(react@18.3.1)': dependencies: react: 18.3.1 @@ -4849,6 +4905,32 @@ snapshots: '@types/react': 18.2.74 '@types/react-dom': 18.2.23 + '@radix-ui/react-menu@2.1.2(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.74)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.74)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.2.74)(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.74 + '@types/react-dom': 18.2.23 + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.2.23)(@types/react@18.2.74)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6979,6 +7061,8 @@ snapshots: lodash-es@4.17.21: {} + lodash.debounce@4.0.8: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -8095,6 +8179,11 @@ snapshots: dependencies: react: 18.3.1 + usehooks-ts@3.1.0(react@18.3.1): + dependencies: + lodash.debounce: 4.0.8 + react: 18.3.1 + util-deprecate@1.0.2: {} uuid@8.3.2: {}