Merge remote-tracking branch 'origin/dev' into fix/make-left-menu-sticky

This commit is contained in:
Chris Holland 2024-10-23 19:55:51 -07:00
commit 9c41df22da
No known key found for this signature in database
GPG key ID: 68B0A864B1B0A0D2
34 changed files with 3514 additions and 2599 deletions

3538
apps/api/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -195,16 +195,16 @@ async def delete_chapter(
# RBAC check # RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session) await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session)
db_session.delete(chapter) # Remove all linked chapter activities
db_session.commit() statement = select(ChapterActivity).where(ChapterActivity.chapter_id == chapter.id)
# Remove all linked activities
statement = select(ChapterActivity).where(ChapterActivity.id == chapter.id)
chapter_activities = db_session.exec(statement).all() chapter_activities = db_session.exec(statement).all()
for chapter_activity in chapter_activities: for chapter_activity in chapter_activities:
db_session.delete(chapter_activity) db_session.delete(chapter_activity)
db_session.commit()
# Delete the chapter
db_session.delete(chapter)
db_session.commit()
return {"detail": "chapter deleted"} return {"detail": "chapter deleted"}

View file

@ -11,10 +11,10 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@hocuspocus/server": "^2.11.3", "@hocuspocus/server": "^2.13.7",
"bun": "^1.0.36", "bun": "^1.1.32",
"typescript": "5.4.4", "typescript": "5.4.4",
"y-protocols": "^1.0.6", "y-protocols": "^1.0.6",
"yjs": "^13.6.14" "yjs": "^13.6.20"
} }
} }

View file

@ -9,77 +9,78 @@ importers:
.: .:
dependencies: dependencies:
'@hocuspocus/server': '@hocuspocus/server':
specifier: ^2.11.3 specifier: ^2.13.7
version: 2.11.3(y-protocols@1.0.6(yjs@13.6.14))(yjs@13.6.14) version: 2.13.7(y-protocols@1.0.6(yjs@13.6.20))(yjs@13.6.20)
bun: bun:
specifier: ^1.0.36 specifier: ^1.1.32
version: 1.1.1 version: 1.1.32
typescript: typescript:
specifier: 5.4.4 specifier: 5.4.4
version: 5.4.4 version: 5.4.4
y-protocols: y-protocols:
specifier: ^1.0.6 specifier: ^1.0.6
version: 1.0.6(yjs@13.6.14) version: 1.0.6(yjs@13.6.20)
yjs: yjs:
specifier: ^13.6.14 specifier: ^13.6.20
version: 13.6.14 version: 13.6.20
packages: packages:
'@hocuspocus/common@2.11.3': '@hocuspocus/common@2.13.7':
resolution: {integrity: sha512-w3UZpW6ZVYIHPEFzZJV3yn1d3EZaXf2m2zU53pwj0AyTBmVD7kB9ZiD6twc9A7NNB1dkqD8c58PbD42+pnNiKQ==} resolution: {integrity: sha512-ROqYfW15XlAGd+qb/FVyp0zUC9Rosv7kdcck9LRMdfW3jT66wK9pDDWL2ily4Qj/zhbLCFtjAUPB4UKln/GYNQ==}
'@hocuspocus/server@2.11.3': '@hocuspocus/server@2.13.7':
resolution: {integrity: sha512-1Vdy4RtJcpffs5I4Ey3M8ulu2f6AbpSDmK4YFG8k3O4EJT7HDSO3Ib5STiRBxlr2LncJeVa2ikwlvwQotsWqew==} resolution: {integrity: sha512-D9juGX9NZoKT9/Ty/HGhaimHJe71DyKbYssC831oetYF33x3WSYV6GY82RhHo9xjKZE6r0Le7jgxgQb+u08slw==}
peerDependencies: peerDependencies:
y-protocols: ^1.0.6 y-protocols: ^1.0.6
yjs: ^13.6.8 yjs: ^13.6.8
'@oven/bun-darwin-aarch64@1.1.1': '@oven/bun-darwin-aarch64@1.1.32':
resolution: {integrity: sha512-RDs5ZMSkcurj4YqPtkcKGYUA46/LDcw7tQ0a4hBI/mtjpYySYmIIYkSeeotl9IJMNcG+ZsHpRc4b7ROFRYhxEw==} resolution: {integrity: sha512-/5lu8RJB/iEoC9pzFOCZLCEmNsEI3DEEyxfItZpih/piNOFhCYmoMvm7mM0YXqSc7pfxYaKukEZEHGeIOiu/3Q==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@oven/bun-darwin-x64-baseline@1.1.1': '@oven/bun-darwin-x64-baseline@1.1.32':
resolution: {integrity: sha512-sJKZqgT9JSbxTPLULHdcYiKy+F4x2gq114FxDwEqn3YVZnBqSO0X9GCqWOa1CNqUaxGvJnNgn+HDkIQlnXVLiA==} resolution: {integrity: sha512-XRP+APoLxrXznL7BEHipAdDu0Mn1RwD9PsaqFu1Be5Dbyi+2bYICuTiN1W9vfZNN3kK8eTyniT7kjwLSTcHbEg==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@oven/bun-darwin-x64@1.1.1': '@oven/bun-darwin-x64@1.1.32':
resolution: {integrity: sha512-RiRbhu9htOML4+81AfHIvjgdVU3jsn+EiyvwuUv5j91vgGrZLkNXebGZXt2eGDDutGzHqvQJqW6sxQ+UNJQi7w==} resolution: {integrity: sha512-Slk4QFHcqZ94mnNlzsCG/9m+gET0mpi13bfizRGv8QNRku8mQTs9brN5XvovYrP5t0rfaZDFMOoaOs6wk6rcDg==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@oven/bun-linux-aarch64@1.1.1': '@oven/bun-linux-aarch64@1.1.32':
resolution: {integrity: sha512-9twn92P90pAwyvC6PzcWv/3a2B2/01TzdCwslWNaI0LdQ3b+sJR4IvdXG1yQI3N2Ne/ticM7eww2eWma4I0LRQ==} resolution: {integrity: sha512-7QdyRCiLHb1MuwDH7iir5xD6gTKWlvMGxvcQhbY/U1djqG/gft4+PaJrSJQcVhqRb143mrMNXIhTgc84O6htMw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@oven/bun-linux-x64-baseline@1.1.1': '@oven/bun-linux-x64-baseline@1.1.32':
resolution: {integrity: sha512-2nXg32DLs0xaZH5GafJ16UqrDr4XGRXTeyZW3PNhplaFY0m3fRDXCqDsXmTvsQoGO/FEtMrEmJSWXbLa7u0B4A==} resolution: {integrity: sha512-zvXOAUTc6cOlUR26NEwh3isV+z2EW4QxFiWoxgvM7eIuZMnZjV22+H+dEzVKJzV58XgfPX//fEPi3Xjg4xXdMA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@oven/bun-linux-x64@1.1.1': '@oven/bun-linux-x64@1.1.32':
resolution: {integrity: sha512-2JPkRTCSXe5w9JvMucx7fgN77yQK+XZ+fY7WlEsZnAR4PjEGImZA12nGNbnxEHM3TmOEivy2PP00nAXeu9LViA==} resolution: {integrity: sha512-JgsQ1ZXKmovQNzf4YdA3WDZVrtEbMlbL43Yjcczovy6lDrhSZkTu1GjzMm5JQnuqNwBC4/eSIgVL27J1Pj6nlA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@oven/bun-windows-x64-baseline@1.1.1': '@oven/bun-windows-x64-baseline@1.1.32':
resolution: {integrity: sha512-3q/THmrP1yA8/YTJoS29Et5a+AxP2jGX96cYHlOZEjoTj/FBNFSuuPVvvFEpjrRkQ8Oz9iNE/C6ltna8WKSUxQ==} resolution: {integrity: sha512-0DgP99AcBfKz3OCS5qO2/8e5TgNhBq8ZiNbPJzQaF4QOpS/Rt+mvFcQUa8x5gTOUPHH9ciaMz0IEs2Qv42eoAw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@oven/bun-windows-x64@1.1.1': '@oven/bun-windows-x64@1.1.32':
resolution: {integrity: sha512-oolhIph8Kah6K/7kPUjcqgc2N5lS6RD4yruwrG2QYhxcYWTh7m36Ngp709l8+trhLLaUyTnvr4MvuiKPl1cRjQ==} resolution: {integrity: sha512-grzRSH/9YIKWUHumygtG9Aen04wJv+v6lbmTvqsfcc/1/BJJFCLf+69EN3PINAIisih1hycxGa9eleVxjL0fqg==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
async-lock@1.4.1: async-lock@1.4.1:
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
bun@1.1.1: bun@1.1.32:
resolution: {integrity: sha512-gV90TkJgHvI50X9BoKQ3zVpPEY6YP0vqOww2uZmsOyckZSRlcFYWhXZwFj6PV8KCFINYs8VZ65m59U2RuFYfWw==} resolution: {integrity: sha512-W+UO0ihRmDHscmTfQAoPPaFLqLaloyBdAIr2iX70v3Vgo+7ZiKeyj7Aa4YDWStxQQmlzD5JP3NG/5TSWAK+1WQ==}
cpu: [arm64, x64]
os: [darwin, linux, win32] os: [darwin, linux, win32]
hasBin: true hasBin: true
@ -90,8 +91,8 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
lib0@0.2.93: lib0@0.2.98:
resolution: {integrity: sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==} resolution: {integrity: sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==}
engines: {node: '>=16'} engines: {node: '>=16'}
hasBin: true hasBin: true
@ -100,12 +101,12 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
uuid@9.0.1: uuid@10.0.0:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true hasBin: true
ws@8.16.0: ws@8.18.0:
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
peerDependencies: peerDependencies:
bufferutil: ^4.0.1 bufferutil: ^4.0.1
@ -122,86 +123,86 @@ packages:
peerDependencies: peerDependencies:
yjs: ^13.0.0 yjs: ^13.0.0
yjs@13.6.14: yjs@13.6.20:
resolution: {integrity: sha512-D+7KcUr0j+vBCUSKXXEWfA+bG4UQBviAwP3gYBhkstkgwy5+8diOPMx0iqLIOxNo/HxaREUimZRxqHGAHCL2BQ==} resolution: {integrity: sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'} engines: {node: '>=16.0.0', npm: '>=8.0.0'}
snapshots: snapshots:
'@hocuspocus/common@2.11.3': '@hocuspocus/common@2.13.7':
dependencies: dependencies:
lib0: 0.2.93 lib0: 0.2.98
'@hocuspocus/server@2.11.3(y-protocols@1.0.6(yjs@13.6.14))(yjs@13.6.14)': '@hocuspocus/server@2.13.7(y-protocols@1.0.6(yjs@13.6.20))(yjs@13.6.20)':
dependencies: dependencies:
'@hocuspocus/common': 2.11.3 '@hocuspocus/common': 2.13.7
async-lock: 1.4.1 async-lock: 1.4.1
kleur: 4.1.5 kleur: 4.1.5
lib0: 0.2.93 lib0: 0.2.98
uuid: 9.0.1 uuid: 10.0.0
ws: 8.16.0 ws: 8.18.0
y-protocols: 1.0.6(yjs@13.6.14) y-protocols: 1.0.6(yjs@13.6.20)
yjs: 13.6.14 yjs: 13.6.20
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
'@oven/bun-darwin-aarch64@1.1.1': '@oven/bun-darwin-aarch64@1.1.32':
optional: true optional: true
'@oven/bun-darwin-x64-baseline@1.1.1': '@oven/bun-darwin-x64-baseline@1.1.32':
optional: true optional: true
'@oven/bun-darwin-x64@1.1.1': '@oven/bun-darwin-x64@1.1.32':
optional: true optional: true
'@oven/bun-linux-aarch64@1.1.1': '@oven/bun-linux-aarch64@1.1.32':
optional: true optional: true
'@oven/bun-linux-x64-baseline@1.1.1': '@oven/bun-linux-x64-baseline@1.1.32':
optional: true optional: true
'@oven/bun-linux-x64@1.1.1': '@oven/bun-linux-x64@1.1.32':
optional: true optional: true
'@oven/bun-windows-x64-baseline@1.1.1': '@oven/bun-windows-x64-baseline@1.1.32':
optional: true optional: true
'@oven/bun-windows-x64@1.1.1': '@oven/bun-windows-x64@1.1.32':
optional: true optional: true
async-lock@1.4.1: {} async-lock@1.4.1: {}
bun@1.1.1: bun@1.1.32:
optionalDependencies: optionalDependencies:
'@oven/bun-darwin-aarch64': 1.1.1 '@oven/bun-darwin-aarch64': 1.1.32
'@oven/bun-darwin-x64': 1.1.1 '@oven/bun-darwin-x64': 1.1.32
'@oven/bun-darwin-x64-baseline': 1.1.1 '@oven/bun-darwin-x64-baseline': 1.1.32
'@oven/bun-linux-aarch64': 1.1.1 '@oven/bun-linux-aarch64': 1.1.32
'@oven/bun-linux-x64': 1.1.1 '@oven/bun-linux-x64': 1.1.32
'@oven/bun-linux-x64-baseline': 1.1.1 '@oven/bun-linux-x64-baseline': 1.1.32
'@oven/bun-windows-x64': 1.1.1 '@oven/bun-windows-x64': 1.1.32
'@oven/bun-windows-x64-baseline': 1.1.1 '@oven/bun-windows-x64-baseline': 1.1.32
isomorphic.js@0.2.5: {} isomorphic.js@0.2.5: {}
kleur@4.1.5: {} kleur@4.1.5: {}
lib0@0.2.93: lib0@0.2.98:
dependencies: dependencies:
isomorphic.js: 0.2.5 isomorphic.js: 0.2.5
typescript@5.4.4: {} typescript@5.4.4: {}
uuid@9.0.1: {} uuid@10.0.0: {}
ws@8.16.0: {} ws@8.18.0: {}
y-protocols@1.0.6(yjs@13.6.14): y-protocols@1.0.6(yjs@13.6.20):
dependencies: dependencies:
lib0: 0.2.93 lib0: 0.2.98
yjs: 13.6.14 yjs: 13.6.20
yjs@13.6.14: yjs@13.6.20:
dependencies: dependencies:
lib0: 0.2.93 lib0: 0.2.98

View file

@ -74,87 +74,84 @@ const CollectionsPage = async (params: any) => {
return ( return (
<GeneralWrapperStyled> <GeneralWrapperStyled>
<div className="flex justify-between"> <div className="flex flex-col space-y-4 mb-8">
<TypeOfContentTitle title="Collections" type="col" /> <div className="flex items-center justify-between">
<AuthenticatedClientElement <TypeOfContentTitle title="Collections" type="col" />
ressourceType="collections" <AuthenticatedClientElement
action="create" ressourceType="collections"
checkMethod="roles" action="create"
orgId={org_id} checkMethod="roles"
> orgId={org_id}
<Link
className="flex justify-center"
href={getUriWithOrg(orgslug, '/collections/new')}
> >
<NewCollectionButton /> <Link href={getUriWithOrg(orgslug, '/collections/new')}>
</Link> <NewCollectionButton />
</AuthenticatedClientElement> </Link>
</div> </AuthenticatedClientElement>
<div className="home_collections flex flex-wrap"> </div>
{collections.map((collection: any) => ( <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div {collections.map((collection: any) => (
className="flex flex-col py-1 px-3" <div key={collection.collection_uuid} className="p-3">
key={collection.collection_uuid} <CollectionThumbnail
> collection={collection}
<CollectionThumbnail orgslug={orgslug}
collection={collection} org_id={org_id}
orgslug={orgslug} />
org_id={org_id} </div>
/> ))}
</div> {collections.length === 0 && (
))} <div className="col-span-full flex justify-center items-center py-8">
{collections.length == 0 && ( <div className="text-center">
<div className="flex mx-auto h-[400px]"> <div className="mb-4">
<div className="flex flex-col justify-center text-center items-center space-y-5"> <svg
<div className="mx-auto"> width="50"
<svg height="50"
width="120" viewBox="0 0 295 295"
height="120" fill="none"
viewBox="0 0 295 295" xmlns="http://www.w3.org/2000/svg"
fill="none" className="mx-auto"
xmlns="http://www.w3.org/2000/svg" >
> <rect
<rect opacity="0.51"
opacity="0.51" x="10"
x="10" y="10"
y="10" width="275"
width="275" height="275"
height="275" rx="75"
rx="75" stroke="#4B5564"
stroke="#4B5564" strokeOpacity="0.15"
strokeOpacity="0.15" strokeWidth="20"
strokeWidth="20" />
/> <path
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564"
fill="#4B5564" fillOpacity="0.08"
fillOpacity="0.08" />
/> </svg>
</svg> </div>
</div> <h1 className="text-xl font-bold text-gray-600 mb-2">
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">
No collections yet No collections yet
</h1> </h1>
<p className="text-lg text-gray-400"> <p className="text-md text-gray-400">
<ContentPlaceHolderIfUserIsNotAdmin <ContentPlaceHolderIfUserIsNotAdmin
text="Create a collection to add content" text="Create a collection to add content"
/> />
</p> </p>
<div className="mt-4">
<AuthenticatedClientElement
checkMethod="roles"
ressourceType="collections"
action="create"
orgId={org_id}
>
<Link href={getUriWithOrg(orgslug, '/collections/new')}>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
</div>
</div> </div>
<AuthenticatedClientElement
checkMethod="roles"
ressourceType="collections"
action="create"
orgId={org_id}
>
<Link href={getUriWithOrg(orgslug, '/collections/new')}>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
</div> </div>
</div> )}
)} </div>
</div> </div>
</GeneralWrapperStyled> </GeneralWrapperStyled>
) )

View file

@ -25,6 +25,7 @@ import AssignmentSubmissionProvider, { useAssignmentSubmission } from '@compone
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { mutate } from 'swr' import { mutate } from 'swr'
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal' import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import { useMediaQuery } from 'usehooks-ts'
interface ActivityClientProps { interface ActivityClientProps {
activityid: string activityid: string
@ -47,6 +48,7 @@ function ActivityClient(props: ActivityClientProps) {
const [bgColor, setBgColor] = React.useState('bg-white') const [bgColor, setBgColor] = React.useState('bg-white')
const [assignment, setAssignment] = React.useState(null) as any; const [assignment, setAssignment] = React.useState(null) as any;
const [markStatusButtonActive, setMarkStatusButtonActive] = React.useState(false); const [markStatusButtonActive, setMarkStatusButtonActive] = React.useState(false);
function getChapterNameByActivityId(course: any, activity_id: any) { function getChapterNameByActivityId(course: any, activity_id: any) {
for (let i = 0; i < course.chapters.length; i++) { for (let i = 0; i < course.chapters.length; i++) {
@ -223,7 +225,7 @@ export function MarkStatus(props: {
}) { }) {
const router = useRouter() const router = useRouter()
const session = useLHSession() as any; const session = useLHSession() as any;
const isMobile = useMediaQuery('(max-width: 768px)')
async function markActivityAsCompleteFront() { async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete( const trail = await markActivityAsComplete(
props.orgslug, props.orgslug,
@ -263,7 +265,7 @@ export function MarkStatus(props: {
<i> <i>
<Check size={17}></Check> <Check size={17}></Check>
</i>{' '} </i>{' '}
<i className="not-italic text-xs font-bold">Mark as complete</i> {!isMobile && <i className="not-italic text-xs font-bold">Mark as complete</i>}
</div> </div>
)} )}
</> </>

View file

@ -18,6 +18,7 @@ import UserAvatar from '@components/Objects/UserAvatar'
import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates' import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates'
import { CourseProvider } from '@components/Contexts/CourseContext' import { CourseProvider } from '@components/Contexts/CourseContext'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useMediaQuery } from 'usehooks-ts'
const CourseClient = (props: any) => { const CourseClient = (props: any) => {
const [user, setUser] = useState<any>({}) const [user, setUser] = useState<any>({})
@ -28,6 +29,7 @@ const CourseClient = (props: any) => {
const course = props.course const course = props.course
const org = useOrg() as any const org = useOrg() as any
const router = useRouter() const router = useRouter()
const isMobile = useMediaQuery('(max-width: 768px)')
function getLearningTags() { function getLearningTags() {
// create array of learnings from a string object (comma separated) // create array of learnings from a string object (comma separated)
@ -72,21 +74,21 @@ const CourseClient = (props: any) => {
<PageLoading></PageLoading> <PageLoading></PageLoading>
) : ( ) : (
<GeneralWrapperStyled> <GeneralWrapperStyled>
<div className="pb-3 flex justify-between items-center"> <div className="pb-3 flex flex-col md:flex-row justify-between items-start md:items-center">
<div> <div>
<p className="text-md font-bold text-gray-400 pb-2">Course</p> <p className="text-md font-bold text-gray-400 pb-2">Course</p>
<h1 className="text-3xl -mt-3 font-bold">{course.name}</h1> <h1 className="text-3xl md:text-3xl -mt-3 font-bold">{course.name}</h1>
</div> </div>
<div> <div className="mt-4 md:mt-0">
<CourseProvider courseuuid={course.course_uuid}> {!isMobile && <CourseProvider courseuuid={course.course_uuid}>
<CourseUpdates /> <CourseUpdates />
</CourseProvider> </CourseProvider>}
</div> </div>
</div> </div>
{props.course?.thumbnail_image && org ? ( {props.course?.thumbnail_image && org ? (
<div <div
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4" className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[200px] md:h-[400px] bg-cover bg-center mb-4"
style={{ style={{
backgroundImage: `url(${getCourseThumbnailMediaDirectory( backgroundImage: `url(${getCourseThumbnailMediaDirectory(
org?.org_uuid, org?.org_uuid,
@ -111,7 +113,7 @@ const CourseClient = (props: any) => {
course={course} course={course}
/> />
<div className="flex flex-row pt-10"> <div className="flex flex-col md:flex-row pt-10">
<div className="course_metadata_left grow space-y-2"> <div className="course_metadata_left grow space-y-2">
<h2 className="py-3 text-2xl font-bold">Description</h2> <h2 className="py-3 text-2xl font-bold">Description</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
@ -141,7 +143,7 @@ const CourseClient = (props: any) => {
</div> </div>
)} )}
<h2 className="py-3 text-2xl font-bold">Course Lessons</h2> <h2 className="py-3 text-xl md:text-2xl font-bold">Course Lessons</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
{course.chapters.map((chapter: any) => { {course.chapters.map((chapter: any) => {
return ( return (
@ -303,20 +305,20 @@ const CourseClient = (props: any) => {
})} })}
</div> </div>
</div> </div>
<div className="course_metadata_right space-y-3 w-72 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="course_metadata_right space-y-3 w-full md:w-72 antialiased flex flex-col md:ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden mt-6 md:mt-0">
{user && ( {user && (
<div className="flex flex-col mx-auto space-y-3 px-2 py-2 items-center"> <div className="flex flex-row md:flex-col mx-auto space-y-0 md:space-y-3 space-x-4 md:space-x-0 px-2 py-2 items-center">
<UserAvatar <UserAvatar
border="border-8" border="border-8"
avatar_url={course.authors[0].avatar_image ? getUserAvatarMediaDirectory(course.authors[0].user_uuid, course.authors[0].avatar_image) : ''} avatar_url={course.authors[0].avatar_image ? getUserAvatarMediaDirectory(course.authors[0].user_uuid, course.authors[0].avatar_image) : ''}
predefined_avatar={course.authors[0].avatar_image ? undefined : 'empty'} predefined_avatar={course.authors[0].avatar_image ? undefined : 'empty'}
width={100} width={isMobile ? 60 : 100}
/> />
<div className="-space-y-2 "> <div className="md:-space-y-2">
<div className="text-[12px] text-neutral-400 font-semibold"> <div className="text-[12px] text-neutral-400 font-semibold">
Author Author
</div> </div>
<div className="text-xl font-bold text-neutral-800"> <div className="text-lg md:text-xl font-bold text-neutral-800">
{course.authors[0].first_name && {course.authors[0].first_name &&
course.authors[0].last_name && ( course.authors[0].last_name && (
<div className="flex space-x-2 items-center"> <div className="flex space-x-2 items-center">
@ -344,14 +346,14 @@ const CourseClient = (props: any) => {
{isCourseStarted() ? ( {isCourseStarted() ? (
<button <button
className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-full md:w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer"
onClick={quitCourse} onClick={quitCourse}
> >
Quit Course Quit Course
</button> </button>
) : ( ) : (
<button <button
className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-black hover:bg-gray-900 hover:cursor-pointer" className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-full md:w-[200px] drop-shadow-md bg-black hover:bg-gray-900 hover:cursor-pointer"
onClick={startCourseUI} onClick={startCourseUI}
> >
Start Course Start Course

View file

@ -29,110 +29,102 @@ function Courses(props: CourseProps) {
} }
return ( return (
<div> <div className="w-full">
<GeneralWrapperStyled> <GeneralWrapperStyled>
<div className="flex flex-wrap justify-between"> <div className="flex flex-col space-y-2 mb-2">
<TypeOfContentTitle title="Courses" type="cou" /> <div className="flex items-center justify-between">
<AuthenticatedClientElement <TypeOfContentTitle title="Courses" type="cou" />
checkMethod="roles" <AuthenticatedClientElement
action="create" checkMethod="roles"
ressourceType="courses" action="create"
orgId={props.org_id} ressourceType="courses"
> orgId={props.org_id}
<Modal >
isDialogOpen={newCourseModal} <Modal
onOpenChange={setNewCourseModal} isDialogOpen={newCourseModal}
minHeight="md" onOpenChange={setNewCourseModal}
dialogContent={ minHeight="md"
<CreateCourseModal dialogContent={
closeModal={closeNewCourseModal} <CreateCourseModal
orgslug={orgslug} closeModal={closeNewCourseModal}
></CreateCourseModal> orgslug={orgslug}
} />
dialogTitle="Create Course" }
dialogDescription="Create a new course" dialogTitle="Create Course"
dialogTrigger={ dialogDescription="Create a new course"
<button> dialogTrigger={
<NewCourseButton /> <button>
</button> <NewCourseButton />
} </button>
/> }
</AuthenticatedClientElement> />
</div> </AuthenticatedClientElement>
</div>
<div className="flex flex-wrap"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{courses.map((course: any) => ( {courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}> <div key={course.course_uuid} className="p-3">
<CourseThumbnail course={course} orgslug={orgslug} /> <CourseThumbnail course={course} orgslug={orgslug} />
</div> </div>
))} ))}
{courses.length == 0 && ( {courses.length === 0 && (
<div className="flex mx-auto h-[400px]"> <div className="col-span-full flex justify-center items-center py-8">
<div className="flex flex-col justify-center text-center items-center space-y-5"> <div className="text-center">
<div className="mx-auto"> <div className="mb-4">
<svg <svg
width="120" width="50"
height="120" height="50"
viewBox="0 0 295 295" viewBox="0 0 295 295"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> className="mx-auto"
<rect >
opacity="0.51" {/* ... SVG content ... */}
x="10" </svg>
y="10" </div>
width="275" <h1 className="text-xl font-bold text-gray-600 mb-2">
height="275"
rx="75"
stroke="#4B5564"
strokeOpacity="0.15"
strokeWidth="20"
/>
<path
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
fill="#4B5564"
fillOpacity="0.08"
/>
</svg>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">
No courses yet No courses yet
</h1> </h1>
{isUserAdmin ? (<p className="text-lg text-gray-400"> <p className="text-md text-gray-400">
Create a course to add content {isUserAdmin ? (
</p>) : (<p className="text-lg text-gray-400"> "Create a course to add content"
No courses available yet ) : (
</p>)} "No courses available yet"
)}
</p>
{isUserAdmin && (
<div className="mt-4">
<AuthenticatedClientElement
action="create"
ressourceType="courses"
checkMethod="roles"
orgId={props.org_id}
>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={
<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
/>
}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>
}
/>
</AuthenticatedClientElement>
</div>
)}
</div> </div>
<AuthenticatedClientElement
action="create"
ressourceType="courses"
checkMethod="roles"
orgId={props.org_id}
>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={
<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>
}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>
}
/>
</AuthenticatedClientElement>
</div> </div>
</div> )}
)} </div>
</div> </div>
</GeneralWrapperStyled> </GeneralWrapperStyled>
</div> </div>

View file

@ -82,48 +82,44 @@ const OrgHomePage = async (params: any) => {
) )
return ( return (
<div> <div className="w-full">
<GeneralWrapperStyled> <GeneralWrapperStyled>
{/* Collections */} {/* Collections */}
<div className="flex items-center "> <div className="flex flex-col space-y-4 mb-8">
<div className="flex grow"> <div className="flex items-center justify-between">
<TypeOfContentTitle title="Collections" type="col" /> <TypeOfContentTitle title="Collections" type="col" />
</div> <AuthenticatedClientElement
<AuthenticatedClientElement checkMethod="roles"
checkMethod="roles" ressourceType="collections"
ressourceType="collections" action="create"
action="create" orgId={org_id}
orgId={org_id}
>
<Link href={getUriWithOrg(orgslug, '/collections/new')}>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
</div>
<div className="home_collections flex flex-wrap">
{collections.map((collection: any) => (
<div
className="flex flex-col py-3 px-3"
key={collection.collection_id}
> >
<CollectionThumbnail <Link href={getUriWithOrg(orgslug, '/collections/new')}>
collection={collection} <NewCollectionButton />
orgslug={orgslug} </Link>
org_id={org.org_id} </AuthenticatedClientElement>
/> </div>
</div> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
))} {collections.map((collection: any) => (
{collections.length == 0 && ( <div key={collection.collection_id} className="flex flex-col p-3">
<div className="flex mx-auto h-[100px]"> <CollectionThumbnail
<div className="flex flex-col justify-center text-center items-center space-y-3"> collection={collection}
<div className="flex flex-col space-y-3"> orgslug={orgslug}
<div className="mx-auto"> org_id={org.org_id}
/>
</div>
))}
{collections.length === 0 && (
<div className="col-span-full flex justify-center items-center py-8">
<div className="text-center">
<div className="mb-4">
<svg <svg
width="50" width="50"
height="50" height="50"
viewBox="0 0 295 295" viewBox="0 0 295 295"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="mx-auto"
> >
<rect <rect
opacity="0.51" opacity="0.51"
@ -143,53 +139,49 @@ const OrgHomePage = async (params: any) => {
/> />
</svg> </svg>
</div> </div>
<div className="space-y-0"> <h1 className="text-xl font-bold text-gray-600 mb-2">
<h1 className="text-xl font-bold text-gray-600"> No collections yet
No collections yet </h1>
</h1> <p className="text-md text-gray-400">
<p className="text-md text-gray-400"> <ContentPlaceHolderIfUserIsNotAdmin
<ContentPlaceHolderIfUserIsNotAdmin text="Create collections to group courses together"
text="Create collections to group courses together" />
/> </p>
</p>
</div>
</div> </div>
</div> </div>
</div> )}
)} </div>
</div> </div>
{/* Courses */} {/* Courses */}
<div className="h-5"></div> <div className="flex flex-col space-y-4">
<div className="flex items-center "> <div className="flex items-center justify-between">
<div className="flex grow">
<TypeOfContentTitle title="Courses" type="cou" /> <TypeOfContentTitle title="Courses" type="cou" />
<AuthenticatedClientElement
ressourceType="courses"
action="create"
checkMethod="roles"
orgId={org_id}
>
<Link href={getUriWithOrg(orgslug, '/courses?new=true')}>
<NewCourseButton />
</Link>
</AuthenticatedClientElement>
</div> </div>
<AuthenticatedClientElement <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
ressourceType="courses" {courses.map((course: any) => (
action="create" <div key={course.course_uuid} className="p-3">
checkMethod="roles" <CourseThumbnail course={course} orgslug={orgslug} />
orgId={org_id} </div>
> ))}
<Link href={getUriWithOrg(orgslug, '/courses?new=true')}> {courses.length === 0 && (
<NewCourseButton /> <div className="col-span-full flex justify-center items-center py-8">
</Link> <div className="text-center">
</AuthenticatedClientElement> <div className="mb-4 ">
</div>
<div className="home_courses flex flex-wrap">
{courses.map((course: any) => (
<div className="py-3 px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 && (
<div className="flex mx-auto h-[300px]">
<div className="flex flex-col justify-center text-center items-center space-y-3">
<div className="flex flex-col space-y-3">
<div className="mx-auto">
<svg <svg
width="50" width="50"
height="50" height="50"
className="mx-auto"
viewBox="0 0 295 295" viewBox="0 0 295 295"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -212,18 +204,16 @@ const OrgHomePage = async (params: any) => {
/> />
</svg> </svg>
</div> </div>
<div className="space-y-0"> <h1 className="text-xl font-bold text-gray-600 mb-2">
<h1 className="text-xl font-bold text-gray-600"> No courses yet
No courses yet </h1>
</h1> <p className="text-md text-gray-400">
<p className="text-md text-gray-400"> <ContentPlaceHolderIfUserIsNotAdmin text='Create courses to add content' />
<ContentPlaceHolderIfUserIsNotAdmin text='Create courses to add content' /> </p>
</p>
</div>
</div> </div>
</div> </div>
</div> )}
)} </div>
</div> </div>
</GeneralWrapperStyled> </GeneralWrapperStyled>
</div> </div>

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

@ -1,12 +1,13 @@
'use client' 'use client'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse' 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 AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton' import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
import Modal from '@components/StyledElements/Modal/Modal' import Modal from '@components/StyledElements/Modal/Modal'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import React from 'react' import React from 'react'
import useAdminStatus from '@components/Hooks/useAdminStatus'
type CourseProps = { type CourseProps = {
orgslug: string orgslug: string
@ -20,114 +21,106 @@ function CoursesHome(params: CourseProps) {
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse) const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse)
const orgslug = params.orgslug const orgslug = params.orgslug
const courses = params.courses const courses = params.courses
const isUserAdmin = useAdminStatus() as any
async function closeNewCourseModal() { async function closeNewCourseModal() {
setNewCourseModal(false) setNewCourseModal(false)
} }
return ( return (
<div className="h-full w-full bg-[#f8f8f8]"> <div className="h-full w-full bg-[#f8f8f8] pl-10 pr-10">
<div> <div className="mb-6">
<div className="pl-10 mr-10 tracking-tighter"> <BreadCrumbs type="courses" />
<BreadCrumbs type="courses" /> <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mt-4">
<div className="w-100 flex justify-between"> <h1 className="text-3xl font-bold mb-4 sm:mb-0">Courses</h1>
<div className="pt-3 flex font-bold text-4xl">Courses</div> <AuthenticatedClientElement
<AuthenticatedClientElement checkMethod="roles"
checkMethod="roles" action="create"
action="create" ressourceType="courses"
ressourceType="courses" orgId={params.org_id}
orgId={params.org_id} >
> <Modal
<Modal isDialogOpen={newCourseModal}
isDialogOpen={newCourseModal} onOpenChange={setNewCourseModal}
onOpenChange={setNewCourseModal} minHeight="md"
minHeight="md" dialogContent={
dialogContent={ <CreateCourseModal
<CreateCourseModal closeModal={closeNewCourseModal}
closeModal={closeNewCourseModal} orgslug={orgslug}
orgslug={orgslug} />
></CreateCourseModal> }
} dialogTitle="Create Course"
dialogTitle="Create Course" dialogDescription="Create a new course"
dialogDescription="Create a new course" dialogTrigger={
dialogTrigger={ <button>
<button> <NewCourseButton />
<NewCourseButton /> </button>
</button> }
} />
/> </AuthenticatedClientElement>
</AuthenticatedClientElement>
</div>
</div> </div>
</div> </div>
<div className="flex flex-wrap mx-8 mt-7">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{courses.map((course: any) => ( {courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}> <div key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} /> <CourseThumbnail customLink={`/dash/courses/course/${removeCoursePrefix(course.course_uuid)}/general`} course={course} orgslug={orgslug} />
</div> </div>
))} ))}
{courses.length == 0 && ( {courses.length === 0 && (
<div className="flex mx-auto h-[400px]"> <div className="col-span-full flex justify-center items-center py-8">
<div className="flex flex-col justify-center text-center items-center space-y-5"> <div className="text-center">
<div className="mx-auto"> <div className="mb-4">
<svg <svg
width="120" width="120"
height="120" height="120"
viewBox="0 0 295 295" viewBox="0 0 295 295"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="mx-auto"
> >
<rect {/* ... SVG content ... */}
opacity="0.51"
x="10"
y="10"
width="275"
height="275"
rx="75"
stroke="#4B5564"
strokeOpacity="0.15"
strokeWidth="20"
/>
<path
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
fill="#4B5564"
fillOpacity="0.08"
/>
</svg> </svg>
</div> </div>
<div className="space-y-0"> <h2 className="text-2xl font-bold text-gray-600 mb-2">
<h1 className="text-3xl font-bold text-gray-600"> No courses yet
No courses yet </h2>
</h1> <p className="text-lg text-gray-400">
<p className="text-lg text-gray-400"> {isUserAdmin ? (
Create a course to add content "Create a course to add content"
</p> ) : (
</div> "No courses available yet"
<AuthenticatedClientElement )}
action="create" </p>
ressourceType="courses" {isUserAdmin && (
checkMethod="roles" <div className="mt-6">
orgId={params.org_id} <AuthenticatedClientElement
> action="create"
<Modal ressourceType="courses"
isDialogOpen={newCourseModal} checkMethod="roles"
onOpenChange={setNewCourseModal} orgId={params.org_id}
minHeight="md" >
dialogContent={ <Modal
<CreateCourseModal isDialogOpen={newCourseModal}
closeModal={closeNewCourseModal} onOpenChange={setNewCourseModal}
orgslug={orgslug} minHeight="md"
></CreateCourseModal> dialogContent={
} <CreateCourseModal
dialogTitle="Create Course" closeModal={closeNewCourseModal}
dialogDescription="Create a new course" orgslug={orgslug}
dialogTrigger={ />
<button> }
<NewCourseButton /> dialogTitle="Create Course"
</button> dialogDescription="Create a new course"
} dialogTrigger={
/> <button>
</AuthenticatedClientElement> <NewCourseButton />
</button>
}
/>
</AuthenticatedClientElement>
</div>
)}
</div> </div>
</div> </div>
)} )}

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

@ -2,13 +2,14 @@
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
import AddUserGroup from '@components/Objects/Modals/Dash/OrgUserGroups/AddUserGroup' import AddUserGroup from '@components/Objects/Modals/Dash/OrgUserGroups/AddUserGroup'
import EditUserGroup from '@components/Objects/Modals/Dash/OrgUserGroups/EditUserGroup'
import ManageUsers from '@components/Objects/Modals/Dash/OrgUserGroups/ManageUsers' import ManageUsers from '@components/Objects/Modals/Dash/OrgUserGroups/ManageUsers'
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal' import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import Modal from '@components/StyledElements/Modal/Modal' import Modal from '@components/StyledElements/Modal/Modal'
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { deleteUserGroup } from '@services/usergroups/usergroups' import { deleteUserGroup } from '@services/usergroups/usergroups'
import { swrFetcher } from '@services/utils/ts/requests' import { swrFetcher } from '@services/utils/ts/requests'
import { SquareUserRound, Users, X } from 'lucide-react' import { Pencil, SquareUserRound, Users, X } from 'lucide-react'
import React from 'react' import React from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import useSWR, { mutate } from 'swr' import useSWR, { mutate } from 'swr'
@ -19,6 +20,7 @@ function OrgUserGroups() {
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const [userGroupManagementModal, setUserGroupManagementModal] = React.useState(false) const [userGroupManagementModal, setUserGroupManagementModal] = React.useState(false)
const [createUserGroupModal, setCreateUserGroupModal] = React.useState(false) const [createUserGroupModal, setCreateUserGroupModal] = React.useState(false)
const [editUserGroupModal, setEditUserGroupModal] = React.useState(false)
const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any
const { data: usergroups } = useSWR( const { data: usergroups } = useSWR(
@ -96,8 +98,24 @@ function OrgUserGroups() {
} }
/> />
</td> </td>
<td className="py-3 px-4 "> <td className="py-3 px-4 flex space-x-2">
<Modal
isDialogOpen={editUserGroupModal}
dialogTrigger={
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md font-bold items-center text-sm text-sky-100">
<Pencil className="size-4" />
<span>Edit</span>
</button>
}
minHeight='sm'
minWidth='sm'
onOpenChange={() => {
setEditUserGroupModal(!editUserGroupModal)
}}
dialogContent={
<EditUserGroup usergroup={usergroup} />
}
/>
<ConfirmationModal <ConfirmationModal
confirmationButtonText="Delete UserGroup" confirmationButtonText="Delete UserGroup"
confirmationMessage="Access to all resources will be removed for all users in this UserGroup. Are you sure you want to delete this UserGroup ?" confirmationMessage="Access to all resources will be removed for all users in this UserGroup. Are you sure you want to delete this UserGroup ?"
@ -105,7 +123,7 @@ function OrgUserGroups() {
dialogTrigger={ dialogTrigger={
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100"> <button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
<X className="w-4 h-4" /> <X className="w-4 h-4" />
<span> Delete</span> <span>Delete</span>
</button> </button>
} }
functionToExecute={() => { functionToExecute={() => {

View file

@ -21,10 +21,14 @@ 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'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip' import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import Link from 'next/link' import Link from 'next/link'
import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { getCourseThumbnailMediaDirectory } from '@services/media/media'
@ -51,6 +55,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
@ -148,6 +153,12 @@ function Editor(props: Editor) {
editable: true, editable: true,
activity: props.activity, activity: props.activity,
}), }),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
// Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true // Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true
...(props.isCollabEnabledOnThisOrg ? [ ...(props.isCollabEnabledOnThisOrg ? [
@ -170,6 +181,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}>
@ -535,6 +561,16 @@ export const EditorContentWrapper = styled.div`
font-weight: 700; font-weight: 700;
} }
} }
&.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
.tableWrapper {
margin: 1.5rem 0;
overflow-x: auto;
}
} }
iframe { iframe {
@ -554,6 +590,53 @@ export const EditorContentWrapper = styled.div`
padding-left: 20px; padding-left: 20px;
list-style-type: decimal; list-style-type: decimal;
} }
table {
border-collapse: collapse;
margin: 0;
overflow: hidden;
table-layout: fixed;
width: 100%;
td,
th {
border: 1px solid rgba(139, 139, 139, 0.4);
box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative;
vertical-align: top;
> * {
margin-bottom: 0;
}
}
th {
background-color: rgba(217, 217, 217, 0.4);
font-weight: bold;
text-align: left;
}
.selectedCell:after {
background: rgba(139, 139, 139, 0.2);
content: "";
left: 0; right: 0; top: 0; bottom: 0;
pointer-events: none;
position: absolute;
z-index: 2;
}
.column-resize-handle {
background-color: #8d78eb;
bottom: -2px;
pointer-events: none;
position: absolute;
right: -2px;
top: 0;
width: 4px;
}
}
` `
export default Editor export default Editor

View file

@ -7,6 +7,11 @@ import {
ArrowRightIcon, ArrowRightIcon,
DividerVerticalIcon, DividerVerticalIcon,
ListBulletIcon, ListBulletIcon,
TableIcon,
RowsIcon,
ColumnsIcon,
SectionIcon,
ContainerIcon,
} from '@radix-ui/react-icons' } from '@radix-ui/react-icons'
import { import {
AlertCircle, AlertCircle,
@ -92,6 +97,34 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<option value="5">Heading 5</option> <option value="5">Heading 5</option>
<option value="6">Heading 6</option> <option value="6">Heading 6</option>
</ToolSelect> </ToolSelect>
<DividerVerticalIcon
style={{ marginTop: 'auto', marginBottom: 'auto', color: 'grey' }}
/>
<ToolBtn content={'Create table'}
onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}
>
<TableIcon/>
</ToolBtn>
<ToolBtn content={'Insert row'}
onClick={() => editor.chain().focus().addRowAfter().run()}
>
<RowsIcon/>
</ToolBtn>
<ToolBtn content={'Insert column'}
onClick={() => editor.chain().focus().addColumnAfter().run()}
>
<ColumnsIcon/>
</ToolBtn>
<ToolBtn content={'Remove column'}
onClick={() => editor.chain().focus().deleteColumn().run()}
>
<ContainerIcon/>
</ToolBtn>
<ToolBtn content={'Remove row'}
onClick={() => editor.chain().focus().deleteRow().run()}
>
<SectionIcon/>
</ToolBtn>
{/* TODO: fix this : toggling only works one-way */} {/* TODO: fix this : toggling only works one-way */}
<DividerVerticalIcon <DividerVerticalIcon
style={{ marginTop: 'auto', marginBottom: 'auto', color: 'grey' }} style={{ marginTop: 'auto', marginBottom: 'auto', color: 'grey' }}

View file

@ -14,54 +14,73 @@ export const Menu = (props: any) => {
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const [feedbackModal, setFeedbackModal] = React.useState(false) const [feedbackModal, setFeedbackModal] = React.useState(false)
const org = useOrg() as any; const org = useOrg() as any;
const [isMenuOpen, setIsMenuOpen] = React.useState(false)
function closeFeedbackModal() { function closeFeedbackModal() {
setFeedbackModal(false) setFeedbackModal(false)
} }
function toggleMenu() {
setIsMenuOpen(!isMenuOpen)
}
return ( return (
<> <>
<div className="backdrop-blur-lg h-[60px] blur-3xl -z-10" style={{}}> <div className="backdrop-blur-lg h-[60px] blur-3xl -z-10"></div>
<div className="backdrop-blur-lg bg-white/90 fixed top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50">
</div> <div className="flex items-center justify-between w-full max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-16 h-full">
<div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50"> <div className="flex items-center space-x-5 md:w-auto w-full">
<div className="flex items-center space-x-5 w-full max-w-screen-2xl mx-auto px-16"> <div className="logo flex md:w-auto w-full justify-center">
<div className="logo flex "> <Link href={getUriWithOrg(orgslug, '/')}>
<Link href={getUriWithOrg(orgslug, '/')}> <div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center">
<div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center"> {org?.logo_image ? (
{org?.logo_image ? ( <img
<img src={`${getOrgLogoMediaDirectory(org.org_uuid, org?.logo_image)}`}
src={`${getOrgLogoMediaDirectory( alt="Learnhouse"
org.org_uuid, style={{ width: 'auto', height: '100%' }}
org?.logo_image className="rounded-md"
)}`} />
alt="Learnhouse" ) : (
style={{ width: 'auto', height: '100%' }} <LearnHouseLogo />
className="rounded-md" )}
/> </div>
) : ( </Link>
<LearnHouseLogo></LearnHouseLogo> </div>
)} <div className="hidden md:flex">
</div> <MenuLinks orgslug={orgslug} />
</Link> </div>
</div> </div>
<div className="links flex grow"> <div className="flex items-center space-x-4">
<div className="hidden md:flex">
<HeaderProfileBox />
</div>
<button
className="md:hidden text-gray-600 focus:outline-none"
onClick={toggleMenu}
>
{isMenuOpen ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
</div>
</div>
<div
className={`fixed inset-x-0 z-40 bg-white/80 backdrop-blur-lg md:hidden shadow-lg transition-all duration-300 ease-in-out ${
isMenuOpen ? 'top-[60px] opacity-100' : '-top-full opacity-0'
}`}
>
<div className="flex flex-col px-4 py-3 space-y-4 justify-center items-center">
<div className='py-4'>
<MenuLinks orgslug={orgslug} /> <MenuLinks orgslug={orgslug} />
</div> </div>
<div className="profile flex items-center"> <div className="border-t border-gray-200">
{/* <Modal
isDialogOpen={feedbackModal}
onOpenChange={setFeedbackModal}
minHeight="sm"
dialogContent={<FeedbackModal></FeedbackModal>}
dialogTitle="Feedback"
dialogDescription="An issue? A suggestion? a bug ? Let us know!"
dialogTrigger={
<div className="feedback cursor-pointer block items-center h-fit p-2 rounded-2xl bg-orange-800 hover:bg-orange-900 text-orange-300 shadow">
<MessageSquareIcon size={12} />
</div>
}
/> */}
<HeaderProfileBox /> <HeaderProfileBox />
</div> </div>
</div> </div>

View file

@ -0,0 +1,104 @@
'use client'
import FormLayout, {
FormField,
FormLabelAndMessage,
Input,
} from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form'
import { useOrg } from '@components/Contexts/OrgContext'
import React from 'react'
import { updateUserGroup } from '@services/usergroups/usergroups'
import { mutate } from 'swr'
import { getAPIUrl } from '@services/config/config'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useFormik } from 'formik'
import toast from 'react-hot-toast'
type EditUserGroupProps = {
usergroup: {
id: number,
name: string,
description: string,
}
}
const validate = (values: any) => {
const errors: any = {}
if (!values.name) {
errors.name = 'Name is Required'
}
return errors
}
function EditUserGroup(props: EditUserGroupProps) {
const org = useOrg() as any;
const session = useLHSession() as any
const access_token = session?.data?.tokens?.access_token;
const [isSubmitting, setIsSubmitting] = React.useState(false)
const formik = useFormik({
initialValues: {
name: props.usergroup.name,
description: props.usergroup.description,
},
validate,
onSubmit: async (values) => {
setIsSubmitting(true)
const res = await updateUserGroup(props.usergroup.id, access_token, values)
if (res.status == 200) {
setIsSubmitting(false)
toast.success(`UserGroup saved successfully`)
mutate(`${getAPIUrl()}usergroups/org/${org.id}`)
} else {
toast.error(`Error saving UserGroup, please retry later.`)
setIsSubmitting(false)
}
},
})
console.log(formik.errors.name)
return (
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage
label="Name"
message={formik.errors.name}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.name}
type="name"
required
/>
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage
label="Description"
message={formik.errors.description}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.description}
type="description"
/>
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
{isSubmitting ? 'Loading...' : 'Save UserGroup'}
</button>
</Form.Submit>
</div>
</FormLayout>
)
}
export default EditUserGroup

View file

@ -25,43 +25,42 @@ const removeCollectionPrefix = (collectionid: string) => {
function CollectionThumbnail(props: PropsType) { function CollectionThumbnail(props: PropsType) {
const org = useOrg() as any const org = useOrg() as any
return ( return (
<div className=""> <div className="group relative overflow-hidden rounded-xl shadow-lg transition-all duration-300 hover:shadow-xl">
<div className="flex flex-row space-x-4 inset-0 ring-1 ring-inset my-auto ring-black/10 rounded-xl shadow-xl relative w-[300px] h-[80px] bg-cover items-center justify-center bg-indigo-600 font-bold text-zinc-50"> <div className="flex h-full w-full items-center justify-between bg-indigo-600 p-4">
<div className="flex -space-x-5"> <div className="flex items-center space-x-5">
{props.collection.courses.slice(0, 2).map((course: any) => ( <div className="flex -space-x-3">
<> {props.collection.courses.slice(0, 3).map((course: any, index: number) => (
<Link <div
href={getUriWithOrg( key={course.course_uuid}
props.orgslug, className="relative h-12 w-12 overflow-hidden rounded-full border-2 border-white shadow-md transition-all duration-300 hover:z-10 hover:scale-110"
'/collection/' + style={{
removeCollectionPrefix(props.collection.collection_uuid) backgroundImage: `url(${getCourseThumbnailMediaDirectory(
)} org?.org_uuid,
> course.course_uuid,
<div course.thumbnail_image
className="inset-0 rounded-full shadow-2xl bg-cover w-12 h-8 justify-center ring-indigo-800 ring-4" )})`,
style={{ backgroundSize: 'cover',
backgroundImage: `url(${getCourseThumbnailMediaDirectory( backgroundPosition: 'center',
org?.org_uuid, zIndex: 3 - index,
course.course_uuid, }}
course.thumbnail_image ></div>
)})`, ))}
}} </div>
></div> <div className="flex flex-col">
</Link> <Link
</> href={getUriWithOrg(
))} props.orgslug,
'/collection/' + removeCollectionPrefix(props.collection.collection_uuid)
)}
className="text-2xl font-bold text-white hover:underline"
>
{props.collection.name}
</Link>
<span className="mt-1 text-sm font-medium text-indigo-200">
{props.collection.courses.length} course{props.collection.courses.length !== 1 ? 's' : ''}
</span>
</div>
</div> </div>
<Link
href={getUriWithOrg(
props.orgslug,
'/collection/' +
removeCollectionPrefix(props.collection.collection_uuid)
)}
>
<h1 className="font-bold text-md justify-center">
{props.collection.name}
</h1>
</Link>
<CollectionAdminEditsArea <CollectionAdminEditsArea
orgslug={props.orgslug} orgslug={props.orgslug}
org_id={props.org_id} org_id={props.org_id}
@ -91,18 +90,18 @@ const CollectionAdminEditsArea = (props: any) => {
orgId={props.org_id} orgId={props.org_id}
checkMethod="roles" checkMethod="roles"
> >
<div className="flex space-x-1 justify-center mx-auto z-20 "> <div className="z-20">
<ConfirmationModal <ConfirmationModal
confirmationMessage="Are you sure you want to delete this collection?" confirmationMessage="Are you sure you want to delete this collection?"
confirmationButtonText="Delete Collection" confirmationButtonText="Delete Collection"
dialogTitle={'Delete ' + props.collection.name + ' ?'} dialogTitle={'Delete ' + props.collection.name + '?'}
dialogTrigger={ dialogTrigger={
<div <button
className="hover:cursor-pointer p-1 px-2 bg-red-600 rounded-xl items-center justify-center flex shadow-xl" className="absolute right-2 top-2 rounded-full bg-red-500 p-2 text-white transition-colors duration-300 hover:bg-red-600"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<X size={10} className="text-rose-200 font-bold" /> <X size={18} />
</div> </button>
} }
functionToExecute={() => deleteCollectionUI(props.collection_uuid)} functionToExecute={() => deleteCollectionUI(props.collection_uuid)}
status="warning" status="warning"

View file

@ -6,146 +6,125 @@ import { getUriWithOrg } from '@services/config/config'
import { deleteCourseFromBackend } from '@services/courses/courses' import { deleteCourseFromBackend } from '@services/courses/courses'
import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { revalidateTags } from '@services/utils/ts/requests' import { revalidateTags } from '@services/utils/ts/requests'
import { BookMinus, FilePenLine, Settings2, EllipsisVertical } from 'lucide-react' import { BookMinus, FilePenLine, Settings2, MoreVertical } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react' import React from 'react'
import toast from 'react-hot-toast' 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 = { type PropsType = {
course: any course: Course
orgslug: string orgslug: string
customLink?: string
} }
// function to remove "course_" from the course_uuid export const removeCoursePrefix = (course_uuid: string) => course_uuid.replace('course_', '')
function removeCoursePrefix(course_uuid: string) {
return course_uuid.replace('course_', '')
}
function CourseThumbnail(props: PropsType) { function CourseThumbnail({ course, orgslug, customLink }: PropsType) {
const router = useRouter() const router = useRouter()
const org = useOrg() as any const org = useOrg() as any
const session = useLHSession() as any; const session = useLHSession() as any
async function deleteCourses(course_uuid: any) { const deleteCourse = async () => {
const toast_loading = toast.loading('Deleting course...') const toastId = toast.loading('Deleting course...')
await deleteCourseFromBackend(course_uuid, session.data?.tokens?.access_token) try {
toast.dismiss(toast_loading) await deleteCourseFromBackend(course.course_uuid, session.data?.tokens?.access_token)
toast.success('Course deleted successfully') await revalidateTags(['courses'], orgslug)
await revalidateTags(['courses'], props.orgslug) toast.success('Course deleted successfully')
router.refresh()
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 ( return (
<div className="relative"> <div className="relative">
<AdminEditsArea <AdminEditOptions
course={props.course} course={course}
orgSlug={props.orgslug} orgSlug={orgslug}
courseId={props.course.course_uuid} deleteCourse={deleteCourse}
deleteCourses={deleteCourses}
/> />
<Link <Link prefetch href={customLink ? customLink : getUriWithOrg(orgslug, `/course/${removeCoursePrefix(course.course_uuid)}`)}>
href={getUriWithOrg( <div
props.orgslug, className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-full aspect-video bg-cover bg-center"
'/course/' + removeCoursePrefix(props.course.course_uuid) style={{ backgroundImage: `url(${thumbnailImage})` }}
)} />
>
{props.course.thumbnail_image ? (
<div
className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover"
style={{
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
org?.org_uuid,
props.course.course_uuid,
props.course.thumbnail_image
)})`,
}}
/>
) : (
<div
className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover"
style={{
backgroundImage: `url('../empty_thumbnail.png')`,
backgroundSize: 'contain',
}}
/>
)}
</Link> </Link>
<div className='flex flex-col w-[250px] pt-3 space-y-2'> <div className='flex flex-col w-full pt-3 space-y-2'>
<h2 className="font-bold text-gray-800 max-h-[80px] h-fit line-clamp-2 leading-tight text-lg capitalize">{props.course.name}</h2> <h2 className="font-bold text-gray-800 line-clamp-2 leading-tight text-lg capitalize">{course.name}</h2>
<h3 className='text-sm text-gray-700 leading-normal line-clamp-3'>{props.course.description}</h3> <p className='text-sm text-gray-700 leading-normal line-clamp-3'>{course.description}</p>
</div> </div>
</div> </div>
) )
} }
const AdminEditsArea = (props: { const AdminEditOptions = ({ course, orgSlug, deleteCourse }: {
course: Course
orgSlug: string orgSlug: string
courseId: string deleteCourse: () => Promise<void>
course: any
deleteCourses: any
}) => { }) => {
return ( return (
<AuthenticatedClientElement <AuthenticatedClientElement
action="update" action="update"
ressourceType="courses" ressourceType="courses"
checkMethod="roles" checkMethod="roles"
orgId={props.course.org_id} orgId={course.org_id}
> >
<div <div className="absolute top-2 right-2 z-20">
className="flex items-center space-x-2 absolute z-20 overflow-hidden rounded-xl pt-0 mx-auto justify-center transform w-full h-[60px] bg-gradient-to-t from-transparent from-10% to-gray-900/60"> <DropdownMenu>
<Link <DropdownMenuTrigger asChild>
href={getUriWithOrg( <button className="p-1 bg-white rounded-full hover:bg-gray-100 transition-colors shadow-md">
props.orgSlug, <MoreVertical size={20} className="text-gray-700" />
'/dash/courses/course/' + </button>
removeCoursePrefix(props.courseId) + </DropdownMenuTrigger>
'/content' <DropdownMenuContent align="end" className="w-56">
)} <DropdownMenuItem asChild>
prefetch <Link prefetch href={getUriWithOrg(orgSlug, `/dash/courses/course/${removeCoursePrefix(course.course_uuid)}/content`)}>
> <FilePenLine className="mr-2 h-4 w-4" /> Edit Content
<div </Link>
className="hover:cursor-pointer p-1 px-4 bg-blue-600 rounded-xl items-center flex shadow-2xl" </DropdownMenuItem>
rel="noopener noreferrer" <DropdownMenuItem asChild>
> <Link prefetch href={getUriWithOrg(orgSlug, `/dash/courses/course/${removeCoursePrefix(course.course_uuid)}/general`)}>
<FilePenLine size={14} className="text-blue-200 font-bold" /> <Settings2 className="mr-2 h-4 w-4" /> Settings
</div> </Link>
</Link> </DropdownMenuItem>
<Link <DropdownMenuItem asChild>
href={getUriWithOrg( <ConfirmationModal
props.orgSlug, confirmationButtonText="Delete Course"
'/dash/courses/course/' + confirmationMessage="Are you sure you want to delete this course?"
removeCoursePrefix(props.courseId) + dialogTitle={`Delete ${course.name}?`}
'/general' dialogTrigger={
)} <button className="w-full text-left flex items-center px-2 py-1 rounded-md text-sm bg-rose-500/10 hover:bg-rose-500/20 transition-colors text-red-600">
prefetch <BookMinus className="mr-4 h-4 w-4" /> Delete Course
> </button>
<div }
className=" hover:cursor-pointer p-1 px-4 bg-gray-800 rounded-xl items-center flex shadow-2xl" functionToExecute={deleteCourse}
rel="noopener noreferrer" status="warning"
> />
<Settings2 size={14} className="text-gray-200 font-bold" /> </DropdownMenuItem>
</div> </DropdownMenuContent>
</Link> </DropdownMenu>
<EllipsisVertical size={14} className='text-gray-200 font-bold' />
<ConfirmationModal
confirmationButtonText="Delete Course"
confirmationMessage="Are you sure you want to delete this course?"
dialogTitle={'Delete ' + props.course.name + ' ?'}
dialogTrigger={
<div
className="hover:cursor-pointer p-1 px-4 bg-rose-600 h-fit rounded-xl items-center flex shadow-2xl"
rel="noopener noreferrer"
>
<BookMinus size={14} className="text-rose-200 font-bold" />
</div>
}
functionToExecute={() => props.deleteCourses(props.courseId)}
status="warning"
></ConfirmationModal>
</div> </div>
</AuthenticatedClientElement> </AuthenticatedClientElement>
) )

View file

@ -1,9 +1,9 @@
function GeneralWrapperStyled({ children }: { children: React.ReactNode }) { function GeneralWrapperStyled({ children }: { children: React.ReactNode }) {
return ( return (
<div className="max-w-screen-2xl mx-auto px-16 py-5 tracking-tight z-50"> <div className="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8 py-5 tracking-tight z-50">
{children} {children}
</div> </div>
) )
} }
export default GeneralWrapperStyled export default GeneralWrapperStyled

View file

@ -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<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View file

@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev --turbo",
"dev-https": "next dev --experimental-https -p 443", "dev-https": "next dev --experimental-https -p 443",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
@ -17,6 +17,7 @@
"@radix-ui/colors": "^0.1.9", "@radix-ui/colors": "^0.1.9",
"@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.2", "@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-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-switch": "^1.1.1",
@ -29,6 +30,10 @@
"@tiptap/extension-code-block-lowlight": "^2.8.0", "@tiptap/extension-code-block-lowlight": "^2.8.0",
"@tiptap/extension-collaboration": "^2.8.0", "@tiptap/extension-collaboration": "^2.8.0",
"@tiptap/extension-collaboration-cursor": "^2.8.0", "@tiptap/extension-collaboration-cursor": "^2.8.0",
"@tiptap/extension-table": "^2.8.0",
"@tiptap/extension-table-cell": "^2.8.0",
"@tiptap/extension-table-header": "^2.8.0",
"@tiptap/extension-table-row": "^2.8.0",
"@tiptap/extension-youtube": "^2.8.0", "@tiptap/extension-youtube": "^2.8.0",
"@tiptap/html": "^2.8.0", "@tiptap/html": "^2.8.0",
"@tiptap/pm": "^2.8.0", "@tiptap/pm": "^2.8.0",
@ -69,6 +74,7 @@
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"unsplash-js": "^7.0.19", "unsplash-js": "^7.0.19",
"usehooks-ts": "^3.1.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.12", "y-prosemirror": "^1.2.12",

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

@ -26,6 +26,9 @@ importers:
'@radix-ui/react-dialog': '@radix-ui/react-dialog':
specifier: ^1.1.2 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) 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': '@radix-ui/react-form':
specifier: ^0.0.3 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) 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)
@ -62,6 +65,18 @@ importers:
'@tiptap/extension-collaboration-cursor': '@tiptap/extension-collaboration-cursor':
specifier: ^2.8.0 specifier: ^2.8.0
version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))(y-prosemirror@1.2.12(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.34.3)(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19)) version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))(y-prosemirror@1.2.12(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.34.3)(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19))
'@tiptap/extension-table':
specifier: ^2.8.0
version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))(@tiptap/pm@2.8.0)
'@tiptap/extension-table-cell':
specifier: ^2.8.0
version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))
'@tiptap/extension-table-header':
specifier: ^2.8.0
version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))
'@tiptap/extension-table-row':
specifier: ^2.8.0
version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))
'@tiptap/extension-youtube': '@tiptap/extension-youtube':
specifier: ^2.8.0 specifier: ^2.8.0
version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0)) version: 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))
@ -182,6 +197,9 @@ importers:
unsplash-js: unsplash-js:
specifier: ^7.0.19 specifier: ^7.0.19
version: 7.0.19 version: 7.0.19
usehooks-ts:
specifier: ^3.1.0
version: 3.1.0(react@18.3.1)
uuid: uuid:
specifier: ^9.0.1 specifier: ^9.0.1
version: 9.0.1 version: 9.0.1
@ -963,6 +981,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true 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': '@radix-ui/react-focus-guards@1.1.1':
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies: peerDependencies:
@ -1034,6 +1065,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true 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': '@radix-ui/react-popper@1.2.0':
resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==}
peerDependencies: peerDependencies:
@ -1554,6 +1598,27 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/core': ^2.7.0 '@tiptap/core': ^2.7.0
'@tiptap/extension-table-cell@2.8.0':
resolution: {integrity: sha512-IZpxONWyOd474L8+k4bHrFNRhbsl9eRwbNs5O877JkVFItc2WUz1DIhbJzjmBRsqExtWQJuOsiqWFab1kpiwGQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-table-header@2.8.0':
resolution: {integrity: sha512-B67A96yMQlG96IFzZBc7D5dnn7O29hcjuDLtjyZkKvU5D/RlFKPMmC9nVphCV3CnbkvEOZUdK9pNaOpen64naw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-table-row@2.8.0':
resolution: {integrity: sha512-Iezej6l7X+WqKzGLmCgAwmpL+QsfjFv1g8yVH5d0/3Pkcj3G9nDn+GSm4bZnbfYFyqInHG94PZ5PMReiALrJtA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-table@2.8.0':
resolution: {integrity: sha512-dm9CitjacXyJuE5SZfV2lUc3uOiP2sxo6fygIzMz7iuxHqQueyONWG+TBkK7HjqzXOiMPsvOf/25NazzIG8HMg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-text-style@2.8.0': '@tiptap/extension-text-style@2.8.0':
resolution: {integrity: sha512-jJp0vcZ2Ty7RvIL0VU6dm1y+fTfXq1lN2GwtYzYM0ueFuESa+Qo8ticYOImyWZ3wGJGVrjn7OV9r0ReW0/NYkQ==} resolution: {integrity: sha512-jJp0vcZ2Ty7RvIL0VU6dm1y+fTfXq1lN2GwtYzYM0ueFuESa+Qo8ticYOImyWZ3wGJGVrjn7OV9r0ReW0/NYkQ==}
peerDependencies: peerDependencies:
@ -2866,6 +2931,9 @@ packages:
lodash-es@4.17.21: lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@ -3876,6 +3944,12 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 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: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -4788,6 +4862,21 @@ snapshots:
'@types/react': 18.2.74 '@types/react': 18.2.74
'@types/react-dom': 18.2.23 '@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)': '@radix-ui/react-focus-guards@1.1.1(@types/react@18.2.74)(react@18.3.1)':
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@ -4849,6 +4938,32 @@ snapshots:
'@types/react': 18.2.74 '@types/react': 18.2.74
'@types/react-dom': 18.2.23 '@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)': '@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: dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -5410,6 +5525,23 @@ snapshots:
dependencies: dependencies:
'@tiptap/core': 2.8.0(@tiptap/pm@2.8.0) '@tiptap/core': 2.8.0(@tiptap/pm@2.8.0)
'@tiptap/extension-table-cell@2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))':
dependencies:
'@tiptap/core': 2.8.0(@tiptap/pm@2.8.0)
'@tiptap/extension-table-header@2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))':
dependencies:
'@tiptap/core': 2.8.0(@tiptap/pm@2.8.0)
'@tiptap/extension-table-row@2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))':
dependencies:
'@tiptap/core': 2.8.0(@tiptap/pm@2.8.0)
'@tiptap/extension-table@2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))(@tiptap/pm@2.8.0)':
dependencies:
'@tiptap/core': 2.8.0(@tiptap/pm@2.8.0)
'@tiptap/pm': 2.8.0
'@tiptap/extension-text-style@2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))': '@tiptap/extension-text-style@2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))':
dependencies: dependencies:
'@tiptap/core': 2.8.0(@tiptap/pm@2.8.0) '@tiptap/core': 2.8.0(@tiptap/pm@2.8.0)
@ -6979,6 +7111,8 @@ snapshots:
lodash-es@4.17.21: {} lodash-es@4.17.21: {}
lodash.debounce@4.0.8: {}
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
lodash@4.17.21: {} lodash@4.17.21: {}
@ -8095,6 +8229,11 @@ snapshots:
dependencies: dependencies:
react: 18.3.1 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: {} util-deprecate@1.0.2: {}
uuid@8.3.2: {} uuid@8.3.2: {}

View file

@ -48,6 +48,19 @@ export async function unLinkUserToUserGroup(
return res return res
} }
export async function updateUserGroup(
usergroup_id: number,
access_token: string,
data: any
) {
const result: any = await fetch(
`${getAPIUrl()}usergroups/${usergroup_id}`,
RequestBodyWithAuthHeader('PUT', data, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function deleteUserGroup( export async function deleteUserGroup(
usergroup_id: number, usergroup_id: number,
access_token: string access_token: string