feat: add mobile dashboard menu, make dashboard pages responsive

This commit is contained in:
swve 2024-10-23 12:03:45 +02:00
parent 56d2007181
commit 3014817d95
13 changed files with 416 additions and 340 deletions

View file

@ -1,8 +1,10 @@
'use client'; '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 AdminAuthorization from '@components/Security/AdminAuthorization'
import { SessionProvider } from 'next-auth/react' import { SessionProvider } from 'next-auth/react'
import React from 'react' import React, { useState, useEffect } from 'react'
import { useMediaQuery } from 'usehooks-ts';
function ClientAdminLayout({ function ClientAdminLayout({
children, children,
@ -11,11 +13,17 @@ function ClientAdminLayout({
children: React.ReactNode children: React.ReactNode
params: any params: any
}) { }) {
const isMobile = useMediaQuery('(max-width: 768px)')
return ( return (
<SessionProvider> <SessionProvider>
<AdminAuthorization authorizationMode="page"> <AdminAuthorization authorizationMode="page">
<div className="flex"> <div className="flex flex-col md:flex-row">
<LeftMenu /> {isMobile ? (
<DashMobileMenu />
) : (
<DashLeftMenu />
)}
<div className="flex w-full">{children}</div> <div className="flex w-full">{children}</div>
</div> </div>
</AdminAuthorization> </AdminAuthorization>
@ -23,4 +31,4 @@ function ClientAdminLayout({
) )
} }
export default ClientAdminLayout export default ClientAdminLayout

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' 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 React, { useEffect } from 'react'
import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'; import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
@ -15,12 +15,29 @@ import { updateActivity } from '@services/courses/activities';
// Lazy Loading // Lazy Loading
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import AssignmentEditorSubPage from './subpages/AssignmentEditorSubPage'; import AssignmentEditorSubPage from './subpages/AssignmentEditorSubPage';
import { useMediaQuery } from 'usehooks-ts';
const AssignmentSubmissionsSubPage = dynamic(() => import('./subpages/AssignmentSubmissionsSubPage')) const AssignmentSubmissionsSubPage = dynamic(() => import('./subpages/AssignmentSubmissionsSubPage'))
function AssignmentEdit() { function AssignmentEdit() {
const params = useParams<{ assignmentuuid: string; }>() const params = useParams<{ assignmentuuid: string; }>()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const [selectedSubPage, setSelectedSubPage] = React.useState(searchParams.get('subpage') || 'editor') 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 (
<div className="h-screen w-full bg-[#f8f8f8] flex items-center justify-center p-4">
<div className="bg-white p-6 rounded-lg shadow-md text-center">
<h2 className="text-xl font-bold mb-4">Desktop Only</h2>
<Monitor className='mx-auto my-5' size={60} />
<p>This page is only accessible from a desktop device.</p>
<p>Please switch to a desktop to view and manage the assignment.</p>
</div>
</div>
)
}
return ( return (
<div className='flex w-full flex-col'> <div className='flex w-full flex-col'>
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}> <AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>

View file

@ -46,19 +46,19 @@ function AssignmentsHome() {
return ( return (
<div className='flex w-full'> <div className='flex w-full'>
<div className='pl-10 mr-10 tracking-tighter flex flex-col space-y-5 w-full'> <div className='pl-4 sm:pl-10 mr-4 sm:mr-10 tracking-tighter flex flex-col space-y-5 w-full'>
<div className='flex flex-col space-y-2'> <div className='flex flex-col space-y-2'>
<BreadCrumbs type="assignments" /> <BreadCrumbs type="assignments" />
<h1 className="pt-3 flex font-bold text-4xl">Assignments</h1> <h1 className="pt-3 flex font-bold text-4xl">Assignments</h1>
</div> </div>
<div className='flex flex-col space-y-3 w-full'> <div className='flex flex-col space-y-3 w-full'>
{courseAssignments.map((assignments: any, index: number) => ( {courseAssignments.map((assignments: any, index: number) => (
<div key={index} className='flex flex-col space-y-2 bg-white nice-shadow p-4 rounded-xl w-full'> <div key={index} className='flex flex-col space-y-2 bg-white nice-shadow p-3 sm:p-4 rounded-xl w-full'>
<div> <div>
<div className='flex space-x-2 items-center justify-between w-full'> <div className='flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 items-start sm:items-center justify-between w-full'>
<div className='flex space-x-2 items-center'> <div className='flex space-x-2 items-center'>
<MiniThumbnail course={courses[index]} /> <MiniThumbnail course={courses[index]} />
<div className='flex flex-col font-bold text-lg '> <div className='flex flex-col font-bold text-lg'>
<p className='bg-gray-200 text-gray-700 px-2 text-xs py-0.5 rounded-full w-fit'>Course</p> <p className='bg-gray-200 text-gray-700 px-2 text-xs py-0.5 rounded-full w-fit'>Course</p>
<p>{courses[index].name}</p> <p>{courses[index].name}</p>
</div> </div>
@ -75,10 +75,9 @@ function AssignmentsHome() {
</Link> </Link>
</div> </div>
{assignments && assignments.map((assignment: any) => ( {assignments && assignments.map((assignment: any) => (
<div key={assignment.assignment_uuid} className='flex mt-3 p-3 rounded flex-row space-x-2 w-full light-shadow justify-between bg-gray-50 items-center'> <div key={assignment.assignment_uuid} className='flex mt-3 p-2 sm:p-3 rounded flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 w-full light-shadow justify-between bg-gray-50 items-start sm:items-center'>
<div className='flex flex-row items-center space-x-2 '> <div className='flex flex-col sm:flex-row items-start sm:items-center space-y-1 sm:space-y-0 sm:space-x-2'>
<div className='flex text-xs font-bold bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full h-fit'> <div className='flex text-xs font-bold bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full h-fit'>
<p>Assignment</p> <p>Assignment</p>
</div> </div>
@ -86,7 +85,6 @@ function AssignmentsHome() {
<div className='flex font-semibold text-gray-600 px-2 py-0.5 rounded outline outline-gray-200/70'>{assignment.description}</div> <div className='flex font-semibold text-gray-600 px-2 py-0.5 rounded outline outline-gray-200/70'>{assignment.description}</div>
</div> </div>
<div className='flex space-x-2 font-bold text-sm items-center'> <div className='flex space-x-2 font-bold text-sm items-center'>
<EllipsisVertical className='text-gray-500' size={17} /> <EllipsisVertical className='text-gray-500' size={17} />
<Link <Link
href={{ href={{
@ -103,7 +101,6 @@ function AssignmentsHome() {
pathname: getUriWithOrg(org.slug, `/dash/assignments/${removeAssignmentPrefix(assignment.assignment_uuid)}`), pathname: getUriWithOrg(org.slug, `/dash/assignments/${removeAssignmentPrefix(assignment.assignment_uuid)}`),
query: { subpage: 'submissions' } query: { subpage: 'submissions' }
}} }}
prefetch prefetch
className='bg-white rounded-full flex space-x-2 nice-shadow items-center px-3 py-0.5'> className='bg-white rounded-full flex space-x-2 nice-shadow items-center px-3 py-0.5'>
<UserRoundPen size={15} /> <UserRoundPen size={15} />
@ -124,10 +121,8 @@ function AssignmentsHome() {
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div> </div>
) )
} }
@ -172,4 +167,4 @@ const MiniThumbnail = (props: { course: any }) => {
} }
export default AssignmentsHome export default AssignmentsHome

View file

@ -7,84 +7,68 @@ import AdminAuthorization from '@components/Security/AdminAuthorization'
function DashboardHome() { function DashboardHome() {
return ( return (
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3"> <div className="flex items-center justify-center mx-auto min-h-screen flex-col p-4 sm:mb-0 mb-16">
<div className="mx-auto pb-10"> <div className="mx-auto pb-6 sm:pb-10">
<Image <Image
alt="learnhouse logo" alt="learnhouse logo"
width={230} width={230}
src={learnhousetextlogo} src={learnhousetextlogo}
></Image> className="w-48 sm:w-auto"
/>
</div> </div>
<AdminAuthorization authorizationMode="component"> <AdminAuthorization authorizationMode="component">
<div className="flex space-x-10"> <div className="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4 lg:space-x-10">
<Link {/* Card components */}
href={`/dash/courses`} <DashboardCard
className="flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer" href="/dash/courses"
> icon={<BookCopy className="mx-auto text-gray-500" size={50} />}
<div className="flex flex-col mx-auto space-y-2"> title="Courses"
<BookCopy className="mx-auto text-gray-500" size={50}></BookCopy> description="Create and manage courses, chapters and activities"
<div className="text-center font-bold text-gray-500">Courses</div> />
<p className="text-center text-sm text-gray-400"> <DashboardCard
Create and manage courses, chapters and ativities{' '} href="/dash/org/settings/general"
</p> icon={<School className="mx-auto text-gray-500" size={50} />}
</div> title="Organization"
</Link> description="Configure your Organization general settings"
<Link />
href={`/dash/org/settings/general`} <DashboardCard
className="flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer" href="/dash/users/settings/users"
> icon={<Users className="mx-auto text-gray-500" size={50} />}
<div className="flex flex-col mx-auto space-y-2"> title="Users"
<School className="mx-auto text-gray-500" size={50}></School> description="Manage your Organization's users, roles"
<div className="text-center font-bold text-gray-500"> />
Organization
</div>
<p className="text-center text-sm text-gray-400">
Configure your Organization general settings{' '}
</p>
</div>
</Link>
<Link
href={`/dash/users/settings/users`}
className="flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer"
>
<div className="flex flex-col mx-auto space-y-2">
<Users className="mx-auto text-gray-500" size={50}></Users>
<div className="text-center font-bold text-gray-500">Users</div>
<p className="text-center text-sm text-gray-400">
Manage your Organization's users, roles{' '}
</p>
</div>
</Link>
</div> </div>
</AdminAuthorization> </AdminAuthorization>
<div className="flex flex-col space-y-10 "> <div className="flex flex-col space-y-6 sm:space-y-10 mt-6 sm:mt-10">
<AdminAuthorization authorizationMode="component"> <AdminAuthorization authorizationMode="component">
<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://university.learnhouse.io/'} href={'https://university.learnhouse.io/'}
target='_blank' 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-4 sm: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"
> >
<University className=" text-gray-100" size={20}></University> <University className="text-gray-100" size={20} />
<div className=" text-sm font-bold text-gray-100"> <div className="text-sm font-bold text-gray-100">
LearnHouse University LearnHouse University
</div> </div>
</Link> </Link>
</div> </div>
<div className="mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full"></div> <div className="mx-auto mt-4 sm:mt-[40px] w-28 h-1 bg-neutral-200 rounded-full"></div>
</AdminAuthorization> </AdminAuthorization>
<Link <Link
href={'/dash/user-account/settings/general'} href={'/dash/user-account/settings/general'}
className="flex bg-white shadow-lg p-[15px] items-center rounded-lg mx-auto hover:scale-105 transition-all ease-linear cursor-pointer" className="flex bg-white shadow-lg p-4 items-center rounded-lg mx-auto hover:scale-105 transition-all ease-linear cursor-pointer max-w-md"
> >
<div className="flex flex-row mx-auto space-x-3 items-center"> <div className="flex flex-col sm:flex-row mx-auto space-y-2 sm:space-y-0 sm:space-x-3 items-center text-center sm:text-left">
<Settings className=" text-gray-500" size={20}></Settings> <Settings className="text-gray-500" size={20} />
<div className=" font-bold text-gray-500">Account Settings</div> <div>
<p className=" text-sm text-gray-400"> <div className="font-bold text-gray-500">Account Settings</div>
Configure your personal settings, passwords, email <p className="text-sm text-gray-400">
</p> Configure your personal settings, passwords, email
</p>
</div>
</div> </div>
</Link> </Link>
</div> </div>
@ -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 (
<Link
href={href}
className="flex bg-white shadow-lg p-6 w-full sm:w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer"
>
<div className="flex flex-col mx-auto space-y-2">
{icon}
<div className="text-center font-bold text-gray-500">{title}</div>
<p className="text-center text-sm text-gray-400">{description}</p>
</div>
</Link>
)
}
export default DashboardHome export default DashboardHome

View file

@ -2,8 +2,9 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import Link from 'next/link' import Link from 'next/link'
import { useMediaQuery } from 'usehooks-ts'
import { getUriWithOrg } from '@services/config/config' 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 BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
@ -22,6 +23,7 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
const org = useOrg() as any const org = useOrg() as any
const [H1Label, setH1Label] = React.useState('') const [H1Label, setH1Label] = React.useState('')
const [H2Label, setH2Label] = React.useState('') const [H2Label, setH2Label] = React.useState('')
const isMobile = useMediaQuery('(max-width: 767px)')
function handleLabels() { function handleLabels() {
if (params.subpage == 'users') { if (params.subpage == 'users') {
@ -46,6 +48,20 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
handleLabels() handleLabels()
}, [session, org, params.subpage, params]) }, [session, org, params.subpage, params])
if (isMobile) {
// TODO: Work on a better mobile experience
return (
<div className="h-screen w-full bg-[#f8f8f8] flex items-center justify-center p-4">
<div className="bg-white p-6 rounded-lg shadow-md text-center">
<h2 className="text-xl font-bold mb-4">Desktop Only</h2>
<Monitor className='mx-auto my-5' size={60} />
<p>This page is only accessible from a desktop device.</p>
<p>Please switch to a desktop to view and manage user settings.</p>
</div>
</div>
)
}
return ( return (
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]"> <div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]"> <div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">

View file

@ -50,14 +50,14 @@ function EditCourseAccess(props: EditCourseAccessProps) {
{courseStructure && ( {courseStructure && (
<div> <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="mx-4 sm:mx-10 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-3 sm: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-lg sm:text-xl text-gray-800">Access to the course</h1>
<h2 className="text-gray-500 text-sm"> <h2 className="text-gray-500 text-xs sm:text-sm">
Choose if you want your course to be publicly available on the internet or only accessible to signed in users Choose if you want your course to be publicly available on the internet or only accessible to signed in users
</h2> </h2>
</div> </div>
<div className="flex space-x-2 mx-auto mb-3"> <div className="flex flex-col sm:flex-row sm:space-x-2 space-y-2 sm:space-y-0 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?"
@ -69,12 +69,12 @@ function EditCourseAccess(props: EditCourseAccessProps) {
Active Active
</div> </div>
)} )}
<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 p-2 sm:p-4">
<Globe className="text-slate-400" size={40} /> <Globe className="text-slate-400" size={32} />
<div className="text-2xl text-slate-700 font-bold"> <div className="text-xl sm:text-2xl text-slate-700 font-bold">
Public Public
</div> </div>
<div className="text-gray-400 text-md tracking-tight w-[500px] leading-5 text-center"> <div className="text-gray-400 text-sm sm:text-md tracking-tight w-full sm:w-[500px] leading-5 text-center">
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>
@ -94,12 +94,12 @@ function EditCourseAccess(props: EditCourseAccessProps) {
Active Active
</div> </div>
)} )}
<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 p-2 sm:p-4">
<Users className="text-slate-400" size={40} /> <Users className="text-slate-400" size={32} />
<div className="text-2xl text-slate-700 font-bold"> <div className="text-xl sm: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-sm sm:text-md tracking-tight w-full sm:w-[500px] leading-5 text-center">
The Course is only accessible to signed in users, additionally 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>
@ -139,42 +139,44 @@ function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
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-3 sm:px-5 py-3 rounded-md mb-3">
<h1 className="font-bold text-xl text-gray-800">UserGroups</h1> <h1 className="font-bold text-lg sm:text-xl text-gray-800">UserGroups</h1>
<h2 className="text-gray-500 text-sm"> <h2 className="text-gray-500 text-xs sm: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"> <div className="overflow-x-auto">
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase"> <table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
<tr className="font-bolder text-sm"> <thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
<th className="py-3 px-4">Name</th> <tr className="font-bolder text-sm">
<th className="py-3 px-4">Actions</th> <th className="py-3 px-4">Name</th>
</tr> <th className="py-3 px-4">Actions</th>
</thead>
<tbody className="mt-5 bg-white rounded-md">
{usergroups?.map((usergroup: any) => (
<tr key={usergroup.invite_code_uuid} className="border-b border-gray-100 text-sm">
<td className="py-3 px-4">{usergroup.name}</td>
<td className="py-3 px-4">
<ConfirmationModal
confirmationButtonText="Delete Link"
confirmationMessage="Users from this UserGroup will no longer have access to this course"
dialogTitle="Unlink UserGroup?"
dialogTrigger={
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
<X className="w-4 h-4" />
<span>Delete link</span>
</button>
}
functionToExecute={() => removeUserGroupLink(usergroup.id)}
status="warning"
/>
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody className="mt-5 bg-white rounded-md">
</table> {usergroups?.map((usergroup: any) => (
<tr key={usergroup.invite_code_uuid} className="border-b border-gray-100 text-sm">
<td className="py-3 px-4">{usergroup.name}</td>
<td className="py-3 px-4">
<ConfirmationModal
confirmationButtonText="Delete Link"
confirmationMessage="Users from this UserGroup will no longer have access to this course"
dialogTitle="Unlink UserGroup?"
dialogTrigger={
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
<X className="w-4 h-4" />
<span>Delete link</span>
</button>
}
functionToExecute={() => removeUserGroupLink(usergroup.id)}
status="warning"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex flex-row-reverse mt-3 mr-2"> <div className="flex flex-row-reverse mt-3 mr-2">
<Modal <Modal
isDialogOpen={userGroupModal} isDialogOpen={userGroupModal}
@ -185,8 +187,8 @@ function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
dialogTitle="Link Course to a UserGroup" dialogTitle="Link Course to a UserGroup"
dialogDescription="Choose a UserGroup to link this course to. Users from this UserGroup will have access to this course." dialogDescription="Choose a UserGroup to link this course to. Users from this UserGroup will have access to this course."
dialogTrigger={ dialogTrigger={
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"> <button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-xs sm:text-sm text-green-100">
<SquareUserRound className="w-4 h-4" /> <SquareUserRound className="w-3 h-3 sm:w-4 sm:h-4" />
<span>Link to a UserGroup</span> <span>Link to a UserGroup</span>
</button> </button>
} }

View file

@ -26,6 +26,7 @@ import { deleteAssignmentUsingActivityUUID, getAssignmentFromActivityUUID } from
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext' import { useCourse } from '@components/Contexts/CourseContext'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useMediaQuery } from 'usehooks-ts'
type ActivitiyElementProps = { type ActivitiyElementProps = {
orgslug: string orgslug: string
@ -50,6 +51,7 @@ function ActivityElement(props: ActivitiyElementProps) {
string | undefined string | undefined
>(undefined) >(undefined)
const activityUUID = props.activity.activity_uuid const activityUUID = props.activity.activity_uuid
const isMobile = useMediaQuery('(max-width: 767px)')
async function deleteActivityUI() { async function deleteActivityUI() {
const toast_loading = toast.loading('Deleting activity...') const toast_loading = toast.loading('Deleting activity...')
@ -110,14 +112,14 @@ function ActivityElement(props: ActivitiyElementProps) {
> >
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className="flex flex-row py-2 my-2 w-full rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 items-center ring-1 ring-inset ring-gray-400/10 shadow-sm" className="flex flex-col sm:flex-row py-2 px-3 my-2 w-full rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 space-y-2 sm:space-y-0 sm:space-x-2 items-center ring-1 ring-inset ring-gray-400/10 nice-shadow"
key={props.activity.id} key={props.activity.id}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
{/* Activity Type Icon */} {/* Activity Type Icon */}
<ActivityTypeIndicator activityType={props.activity.activity_type} /> <ActivityTypeIndicator activityType={props.activity.activity_type} isMobile={isMobile} />
{/* Centered Activity Name */} {/* Centered Activity Name */}
<div className="grow items-center space-x-2 flex mx-auto justify-center"> <div className="grow items-center space-x-2 flex mx-auto justify-center">
@ -143,13 +145,11 @@ function ActivityElement(props: ActivitiyElementProps) {
onClick={() => updateActivityName(props.activity.id)} onClick={() => updateActivityName(props.activity.id)}
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900" className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
> >
<Save <Save size={12} />
size={12}
/>
</button> </button>
</div> </div>
) : ( ) : (
<p className="first-letter:uppercase"> {props.activity.name} </p> <p className="first-letter:uppercase text-center sm:text-left"> {props.activity.name} </p>
)} )}
<Pencil <Pencil
onClick={() => setSelectedActivity(props.activity.id)} onClick={() => setSelectedActivity(props.activity.id)}
@ -157,65 +157,60 @@ function ActivityElement(props: ActivitiyElementProps) {
/> />
</div> </div>
{/* Edit, View, Publish, and Delete Buttons */}
{/* Edit and View Button */} <div className="flex flex-wrap justify-center sm:justify-end gap-2 w-full sm:w-auto">
<div className="flex basis-1/2 justify-end"> <ActivityElementOptions activity={props.activity} isMobile={isMobile} />
<div className="flex flex-row space-x-2"> {/* Publishing */}
<ActivityElementOptions activity={props.activity} /> <button
{/* Publishing */} className={`p-1 px-2 sm:px-3 border shadow-md rounded-md font-bold text-xs flex items-center space-x-1 transition-colors duration-200 ${
<div !props.activity.published
className={`hover:cursor-pointer p-1 px-3 border shadow-lg rounded-md font-bold text-xs flex items-center space-x-1 ${!props.activity.published ? 'bg-gradient-to-bl text-green-800 from-green-400/50 to-lime-200/80 border-green-600/10 hover:from-green-500/50 hover:to-lime-300/80'
? 'bg-gradient-to-bl text-green-800 from-green-400/50 to-lime-200/80 border-green-600/10 shadow-green-900/10' : 'bg-gradient-to-bl text-gray-800 from-gray-400/50 to-gray-200/80 border-gray-600/10 hover:from-gray-500/50 hover:to-gray-300/80'
: 'bg-gradient-to-bl text-gray-800 from-gray-400/50 to-gray-200/80 border-gray-600/10 shadow-gray-900/10' }`}
}`} onClick={() => changePublicStatus()}
rel="noopener noreferrer" >
onClick={() => changePublicStatus()} {!props.activity.published ? (
> <Globe strokeWidth={2} size={12} className="text-green-600" />
{!props.activity.published ? ( ) : (
<Globe strokeWidth={2} size={12} className="text-green-600" /> <Lock strokeWidth={2} size={12} className="text-gray-600" />
) : ( )}
<Lock strokeWidth={2} size={12} className="text-gray-600" /> <span>{!props.activity.published ? 'Publish' : 'Unpublish'}</span>
)} </button>
<span>{!props.activity.published ? 'Publish' : 'Unpublish'}</span> <Link
</div> href={
<Link getUriWithOrg(props.orgslug, '') +
href={ `/course/${props.course_uuid.replace(
getUriWithOrg(props.orgslug, '') + 'course_',
`/course/${props.course_uuid.replace( ''
'course_', )}/activity/${props.activity.activity_uuid.replace(
'' 'activity_',
)}/activity/${props.activity.activity_uuid.replace( ''
'activity_', )}`
'' }
)}` prefetch
} className="p-1 px-2 sm:px-3 bg-gradient-to-bl text-cyan-800 from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-md rounded-md font-bold text-xs flex items-center space-x-1 transition-colors duration-200 hover:from-sky-500/50 hover:to-cyan-300/80"
prefetch rel="noopener noreferrer"
className=" hover:cursor-pointer p-1 px-3 bg-gradient-to-bl text-cyan-800 from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-cyan-900/10 shadow-lg rounded-md font-bold text-xs flex items-center space-x-1" >
rel="noopener noreferrer" <Eye strokeWidth={2} size={12} className="text-sky-600" />
> <span>Preview</span>
<Eye strokeWidth={2} size={12} className="text-sky-600" /> </Link>
<span>Preview</span>
</Link>
</div>
{/* Delete Button */} {/* Delete Button */}
<div className="flex flex-row pr-3 space-x-1 items-center"> <ConfirmationModal
<MoreVertical size={15} className="text-gray-300" /> confirmationMessage="Are you sure you want to delete this activity ?"
<ConfirmationModal confirmationButtonText="Delete Activity"
confirmationMessage="Are you sure you want to delete this activity ?" dialogTitle={'Delete ' + props.activity.name + ' ?'}
confirmationButtonText="Delete Activity" dialogTrigger={
dialogTitle={'Delete ' + props.activity.name + ' ?'} <button
dialogTrigger={ className="p-1 px-2 sm:px-3 bg-red-600 rounded-md flex items-center space-x-1 shadow-md transition-colors duration-200 hover:bg-red-700"
<div rel="noopener noreferrer"
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md" >
rel="noopener noreferrer" <X size={15} className="text-rose-200 font-bold" />
> {!isMobile && <span className="text-rose-200 font-bold text-xs">Delete</span>}
<X size={15} className="text-rose-200 font-bold" /> </button>
</div> }
} functionToExecute={() => deleteActivityUI()}
functionToExecute={() => deleteActivityUI()} status="warning"
status="warning" />
></ConfirmationModal>
</div>
</div> </div>
</div> </div>
)} )}
@ -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] const {displayName, Icon} = ACTIVITIES[activityType]
return ( return (
<div className="px-3 text-gray-300 space-x-1 w-28 flex"> <div className={`px-3 text-gray-300 space-x-1 w-28 flex ${isMobile ? 'flex-col' : ''}`}>
<div className="flex space-x-2 items-center"> <div className="flex space-x-2 items-center">
<Icon className="size-4" />{' '} <Icon className="size-4" />{' '}
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle"> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">
@ -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 [assignmentUUID, setAssignmentUUID] = useState('');
const org = useOrg() as any; const org = useOrg() as any;
const course = useCourse() as any; const course = useCourse() as any;
@ -299,11 +294,11 @@ const ActivityElementOptions = ({ activity }: any) => {
)}/edit` )}/edit`
} }
prefetch prefetch
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center" className={`hover:cursor-pointer p-1 ${isMobile ? 'px-2' : 'px-3'} bg-sky-700 rounded-md items-center`}
target='_blank' // hotfix for an editor prosemirror bug target='_blank'
> >
<div className="text-sky-100 font-bold text-xs flex items-center space-x-1"> <div className="text-sky-100 font-bold text-xs flex items-center space-x-1">
<FilePenLine size={12} /> <span>Edit Page</span> <FilePenLine size={12} /> <span>Edit Page</span>
</div> </div>
</Link> </Link>
</> </>
@ -316,10 +311,10 @@ const ActivityElementOptions = ({ activity }: any) => {
`/dash/assignments/${assignmentUUID}` `/dash/assignments/${assignmentUUID}`
} }
prefetch 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`}
> >
<div className="text-sky-100 font-bold text-xs flex items-center space-x-1"> <div className="text-sky-100 font-bold text-xs flex items-center space-x-1">
<FilePenLine size={12} /> <span>Edit Assignment</span> <FilePenLine size={12} /> {!isMobile && <span>Edit Assignment</span>}
</div> </div>
</Link> </Link>
</> </>

View file

@ -6,6 +6,7 @@ import {
Pencil, Pencil,
Save, Save,
X, X,
Trash2,
} from 'lucide-react' } from 'lucide-react'
import React from 'react' import React from 'react'
import { Draggable, Droppable } from 'react-beautiful-dnd' import { Draggable, Droppable } from 'react-beautiful-dnd'
@ -71,27 +72,27 @@ function ChapterElement(props: ChapterElementProps) {
> >
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 pt-6" className="mx-2 sm:mx-4 md:mx-6 lg:mx-10 bg-white rounded-xl nice-shadow px-3 sm:px-4 md:px-6 pt-4 sm:pt-6"
key={props.chapter.chapter_uuid} key={props.chapter.chapter_uuid}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
<div className="flex font-bold text-md items-center space-x-2 pb-3"> <div className="flex flex-wrap items-center justify-between pb-3">
<div className="flex grow text-lg space-x-3 items-center rounded-md "> <div className="flex grow items-center space-x-2 mb-2 sm:mb-0">
<div className="bg-neutral-100 rounded-md p-2"> <div className="bg-neutral-100 rounded-md p-2">
<Hexagon <Hexagon
strokeWidth={3} strokeWidth={3}
size={16} size={16}
className="text-neutral-600 " className="text-neutral-600"
/> />
</div> </div>
<div className="flex space-x-2 items-center"> <div className="flex items-center space-x-2">
{selectedChapter === props.chapter.id ? ( {selectedChapter === props.chapter.id ? (
<div className="chapter-modification-zone bg-neutral-100 py-1 px-4 rounded-lg space-x-3"> <div className="chapter-modification-zone bg-neutral-100 py-1 px-2 sm:px-4 rounded-lg flex items-center space-x-2">
<input <input
type="text" type="text"
className="bg-transparent outline-none text-sm text-neutral-700" className="bg-transparent outline-none text-sm text-neutral-700 w-full max-w-[150px] sm:max-w-none"
placeholder="Chapter name" placeholder="Chapter name"
value={ value={
modifiedChapter modifiedChapter
@ -109,14 +110,11 @@ function ChapterElement(props: ChapterElementProps) {
onClick={() => updateChapterName(props.chapter.id)} onClick={() => updateChapterName(props.chapter.id)}
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900" className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
> >
<Save <Save size={15} />
size={15}
onClick={() => updateChapterName(props.chapter.id)}
/>
</button> </button>
</div> </div>
) : ( ) : (
<p className="text-neutral-700 first-letter:uppercase"> <p className="text-neutral-700 first-letter:uppercase text-sm sm:text-base">
{props.chapter.name} {props.chapter.name}
</p> </p>
)} )}
@ -127,23 +125,24 @@ function ChapterElement(props: ChapterElementProps) {
/> />
</div> </div>
</div> </div>
<MoreVertical size={15} className="text-gray-300" /> <div className="flex items-center space-x-2">
<ConfirmationModal <MoreVertical size={15} className="text-gray-300" />
confirmationButtonText="Delete Chapter" <ConfirmationModal
confirmationMessage="Are you sure you want to delete this chapter?" confirmationButtonText="Delete Chapter"
dialogTitle={'Delete ' + props.chapter.name + ' ?'} confirmationMessage="Are you sure you want to delete this chapter?"
dialogTrigger={ dialogTitle={'Delete ' + props.chapter.name + ' ?'}
<div dialogTrigger={
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm" <button
rel="noopener noreferrer" className="hover:cursor-pointer p-1 px-2 sm:px-3 bg-red-600 rounded-md shadow flex items-center text-rose-100 text-sm"
> rel="noopener noreferrer"
<X size={15} className="text-rose-200 font-bold" /> >
<p>Delete Chapter</p> <Trash2 size={15} className="text-rose-200" />
</div> </button>
} }
functionToExecute={() => deleteChapterUI()} functionToExecute={() => deleteChapterUI()}
status="warning" status="warning"
></ConfirmationModal> />
</div>
</div> </div>
<Droppable <Droppable
key={props.chapter.chapter_uuid} key={props.chapter.chapter_uuid}

View file

@ -93,7 +93,7 @@ function OrgEditGeneral() {
useEffect(() => {}, [org]) useEffect(() => {}, [org])
return ( return (
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5"> <div className="sm:ml-10 sm:mr-10 ml-0 mr-0 mx-auto bg-white rounded-xl shadow-sm px-6 py-5 sm:mb-0 mb-16">
<Toaster /> <Toaster />
<Formik <Formik
enableReinitialize enableReinitialize
@ -107,8 +107,8 @@ function OrgEditGeneral() {
> >
{({ isSubmitting }) => ( {({ isSubmitting }) => (
<Form> <Form>
<div className="flex space-x-8"> <div className="flex flex-col lg:flex-row lg:space-x-8">
<div className="max-w-md flex-grow"> <div className="w-full lg:w-1/2 mb-8 lg:mb-0">
<label className="block mb-2 font-bold" htmlFor="name"> <label className="block mb-2 font-bold" htmlFor="name">
Name Name
</label> </label>
@ -149,24 +149,24 @@ function OrgEditGeneral() {
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-black" className="w-full sm:w-auto px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-black"
> >
Submit Submit
</button> </button>
</div> </div>
<div className="flex flex-col grow space-y-3"> <div className="w-full lg:w-1/2">
<Tabs defaultValue="logo" className="w-full "> <Tabs defaultValue="logo" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-20"> <TabsList className="grid w-full grid-cols-2 mb-6 sm:mb-10">
<TabsTrigger value="logo">Logo</TabsTrigger> <TabsTrigger value="logo">Logo</TabsTrigger>
<TabsTrigger value="thumbnail">Thumbnail</TabsTrigger> <TabsTrigger value="thumbnail">Thumbnail</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="logo"> <TabsContent value="logo">
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20"> <div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-4 sm:mx-10">
<div className="flex flex-col justify-center items-center mt-10"> <div className="flex flex-col justify-center items-center mt-6 sm:mt-10">
<div <div
className="w-[200px] h-[100px] bg-contain bg-no-repeat bg-center rounded-lg nice-shadow bg-white" className="w-[150px] sm:w-[200px] h-[75px] sm:h-[100px] bg-contain bg-no-repeat bg-center rounded-lg nice-shadow bg-white"
style={{ backgroundImage: `url(${localLogo || getOrgLogoMediaDirectory(org?.org_uuid, org?.logo_image)})` }} style={{ backgroundImage: `url(${localLogo || getOrgLogoMediaDirectory(org?.org_uuid, org?.logo_image)})` }}
/> />
</div> </div>
@ -189,16 +189,16 @@ function OrgEditGeneral() {
</div> </div>
<div className="flex text-xs space-x-2 items-center text-gray-500 justify-center"> <div className="flex text-xs space-x-2 items-center text-gray-500 justify-center">
<Info size={13} /> <Info size={13} />
<p>Accepts PNG , JPG</p> <p>Accepts PNG, JPG</p>
</div> </div>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="thumbnail"> <TabsContent value="thumbnail">
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20"> <div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-4 sm:mx-10">
<div className="flex flex-col justify-center items-center mt-10"> <div className="flex flex-col justify-center items-center mt-6 sm:mt-10">
<div <div
className="w-[200px] h-[100px] bg-contain bg-no-repeat bg-center rounded-lg nice-shadow bg-white" className="w-[150px] sm:w-[200px] h-[75px] sm:h-[100px] bg-contain bg-no-repeat bg-center rounded-lg nice-shadow bg-white"
style={{ backgroundImage: `url(${localThumbnail || getOrgThumbnailMediaDirectory(org?.org_uuid, org?.thumbnail_image)})` }} style={{ backgroundImage: `url(${localThumbnail || getOrgThumbnailMediaDirectory(org?.org_uuid, org?.thumbnail_image)})` }}
/> />
</div> </div>

View file

@ -12,7 +12,7 @@ import AdminAuthorization from '@components/Security/AdminAuthorization'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
function LeftMenu() { function DashLeftMenu() {
const org = useOrg() as any const org = useOrg() as any
const session = useLHSession() as any const session = useLHSession() as any
const [loading, setLoading] = React.useState(true) const [loading, setLoading] = React.useState(true)
@ -176,4 +176,4 @@ function LeftMenu() {
) )
} }
export default LeftMenu export default DashLeftMenu

View file

@ -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 (
<div className="fixed bottom-0 left-0 right-0 bg-black/90 backdrop-blur-lg text-white shadow-xl">
<div className="flex justify-around items-center h-16 px-2">
<AdminAuthorization authorizationMode="component">
<ToolTip content={'Home'} slateBlack sideOffset={8} side="top">
<Link href={`/`} className="flex flex-col items-center p-2">
<Home size={20} />
<span className="text-xs mt-1">Home</span>
</Link>
</ToolTip>
<ToolTip content={'Courses'} slateBlack sideOffset={8} side="top">
<Link href={`/dash/courses`} className="flex flex-col items-center p-2">
<BookCopy size={20} />
<span className="text-xs mt-1">Courses</span>
</Link>
</ToolTip>
<ToolTip content={'Assignments'} slateBlack sideOffset={8} side="top">
<Link href={`/dash/assignments`} className="flex flex-col items-center p-2">
<Backpack size={20} />
<span className="text-xs mt-1">Assignments</span>
</Link>
</ToolTip>
<ToolTip content={'Users'} slateBlack sideOffset={8} side="top">
<Link href={`/dash/users/settings/users`} className="flex flex-col items-center p-2">
<Users size={20} />
<span className="text-xs mt-1">Users</span>
</Link>
</ToolTip>
<ToolTip content={'Organization'} slateBlack sideOffset={8} side="top">
<Link href={`/dash/org/settings/general`} className="flex flex-col items-center p-2">
<School size={20} />
<span className="text-xs mt-1">Org</span>
</Link>
</ToolTip>
</AdminAuthorization>
<ToolTip content={session.data.user.username + "'s Settings"} slateBlack sideOffset={8} side="top">
<Link href={'/dash/user-account/settings/general'} className="flex flex-col items-center p-2">
<Settings size={20} />
<span className="text-xs mt-1">Settings</span>
</Link>
</ToolTip>
</div>
</div>
)
}
export default DashMobileMenu

View file

@ -40,7 +40,7 @@ function UserEditGeneral() {
useEffect(() => { }, [session, session.data]) useEffect(() => { }, [session, session.data])
return ( return (
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5"> <div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5 sm:mb-0 mb-16">
{session.data.user && ( {session.data.user && (
<Formik <Formik
enableReinitialize enableReinitialize
@ -59,84 +59,53 @@ function UserEditGeneral() {
}} }}
> >
{({ isSubmitting }) => ( {({ isSubmitting }) => (
<div className="flex space-x-8"> <div className="flex flex-col lg:flex-row gap-8">
<Form className="max-w-md"> <Form className="flex-1 min-w-0">
<label className="block mb-2 font-bold" htmlFor="email"> <div className="space-y-4">
Email {[
</label> { label: 'Email', name: 'email', type: 'email' },
<Field { label: 'Username', name: 'username', type: 'text' },
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" { label: 'First Name', name: 'first_name', type: 'text' },
type="email" { label: 'Last Name', name: 'last_name', type: 'text' },
name="email" { label: 'Bio', name: 'bio', type: 'text' },
/> ].map((field) => (
<div key={field.name}>
<label className="block mb-2 font-bold" htmlFor="username"> <label className="block mb-2 font-bold" htmlFor={field.name}>
Username {field.label}
</label> </label>
<Field <Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="username" type={field.type}
name="username" name={field.name}
/> />
</div>
<label className="block mb-2 font-bold" htmlFor="first_name"> ))}
First Name </div>
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="first_name"
name="first_name"
/>
<label className="block mb-2 font-bold" htmlFor="last_name">
Last Name
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="last_name"
name="last_name"
/>
<label className="block mb-2 font-bold" htmlFor="bio">
Bio
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="bio"
name="bio"
/>
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500" className="mt-6 px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
Submit Submit
</button> </button>
</Form> </Form>
<div className="flex flex-col grow justify-center align-middle space-y-3"> <div className="flex-1 min-w-0">
<label className="flex mx-auto mb-2 font-bold ">Avatar</label> <div className="flex flex-col items-center space-y-4">
{error && ( <label className="font-bold">Avatar</label>
<div className="flex justify-center mx-auto bg-red-200 rounded-md text-red-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm"> {error && (
<FileWarning size={16} className="mr-2" /> <div className="flex items-center bg-red-200 rounded-md text-red-950 px-4 py-2 text-sm">
<div className="text-sm font-semibold first-letter:uppercase"> <FileWarning size={16} className="mr-2" />
{error} <span className="font-semibold first-letter:uppercase">{error}</span>
</div> </div>
</div> )}
)} {success && (
{success && ( <div className="flex items-center bg-green-200 rounded-md text-green-950 px-4 py-2 text-sm">
<div className="flex justify-center mx-auto bg-green-200 rounded-md text-green-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm"> <Check size={16} className="mr-2" />
<Check size={16} className="mr-2" /> <span className="font-semibold first-letter:uppercase">{success}</span>
<div className="text-sm font-semibold first-letter:uppercase">
{success}
</div> </div>
</div> )}
)} <div className="w-full max-w-xs bg-gray-50 rounded-xl outline outline-1 outline-gray-200 shadow p-6">
<div className="flex flex-col space-y-3"> <div className="flex flex-col items-center space-y-4">
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20">
<div className="flex flex-col justify-center items-center mt-10">
{localAvatar ? ( {localAvatar ? (
<UserAvatar <UserAvatar
border="border-8" border="border-8"
@ -146,42 +115,32 @@ function UserEditGeneral() {
) : ( ) : (
<UserAvatar border="border-8" width={100} /> <UserAvatar border="border-8" width={100} />
)} )}
</div> {isLoading ? (
{isLoading ? ( <div className="font-bold animate-pulse antialiased bg-green-200 text-gray text-sm rounded-md px-4 py-2 flex items-center">
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div className="font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
<ArrowBigUpDash size={16} className="mr-2" /> <ArrowBigUpDash size={16} className="mr-2" />
<span>Uploading</span> <span>Uploading</span>
</div> </div>
</div> ) : (
) : ( <>
<div className="flex justify-center items-center"> <input
<input type="file"
type="file" id="fileInput"
id="fileInput" className="hidden"
style={{ display: 'none' }} onChange={handleFileChange}
onChange={handleFileChange} />
/> <button
<button className="font-bold antialiased text-gray text-sm rounded-md px-4 py-2 flex items-center"
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex" onClick={() => document.getElementById('fileInput')?.click()}
onClick={() => >
document.getElementById('fileInput')?.click() <UploadCloud size={16} className="mr-2" />
} <span>Change Avatar</span>
> </button>
<UploadCloud size={16} className="mr-2" /> </>
<span>Change Avatar</span> )}
</button> </div>
</div>
)}
</div> </div>
<div className="flex text-xs space-x-2 items-center text-gray-500 justify-center"> <div className="flex items-center text-xs text-gray-500">
<Info size={13} /> <Info size={13} className="mr-2" />
<p>Recommended size 100x100</p> <p>Recommended size 100x100</p>
</div> </div>
</div> </div>

View file

@ -21,7 +21,7 @@ import WarningCallout from './Extensions/Callout/Warning/WarningCallout'
import ImageBlock from './Extensions/Image/ImageBlock' import ImageBlock from './Extensions/Image/ImageBlock'
import Youtube from '@tiptap/extension-youtube' import Youtube from '@tiptap/extension-youtube'
import VideoBlock from './Extensions/Video/VideoBlock' 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 MathEquationBlock from './Extensions/MathEquation/MathEquationBlock'
import PDFBlock from './Extensions/PDF/PDFBlock' import PDFBlock from './Extensions/PDF/PDFBlock'
import QuizBlock from './Extensions/Quiz/QuizBlock' import QuizBlock from './Extensions/Quiz/QuizBlock'
@ -51,6 +51,7 @@ import { getUriWithOrg } from '@services/config/config'
import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects' import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects'
import Badges from './Extensions/Badges/Badges' import Badges from './Extensions/Badges/Badges'
import Buttons from './Extensions/Buttons/Buttons' import Buttons from './Extensions/Buttons/Buttons'
import { useMediaQuery } from 'usehooks-ts'
interface Editor { interface Editor {
content: string content: string
@ -170,6 +171,21 @@ function Editor(props: Editor) {
content: props.isCollabEnabledOnThisOrg ? null : props.content, content: props.isCollabEnabledOnThisOrg ? null : props.content,
}) })
const isMobile = useMediaQuery('(max-width: 767px)')
if (isMobile) {
// TODO: Work on a better editor mobile experience
return (
<div className="h-screen w-full bg-[#f8f8f8] flex items-center justify-center p-4">
<div className="bg-white p-6 rounded-lg shadow-md text-center">
<h2 className="text-xl font-bold mb-4">Desktop Only</h2>
<Monitor className='mx-auto my-5' size={60} />
<p>The editor is only accessible from a desktop device.</p>
<p>Please switch to a desktop to view.</p>
</div>
</div>
)
}
return ( return (
<Page> <Page>
<CourseProvider courseuuid={props.course.course_uuid}> <CourseProvider courseuuid={props.course.course_uuid}>