feat: make external pages responsive

This commit is contained in:
swve 2024-10-20 23:12:33 +02:00
parent 5056f53c32
commit 56d2007181
12 changed files with 758 additions and 508 deletions

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
@ -48,6 +49,7 @@ function ActivityClient(props: ActivityClientProps) {
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++) {
let chapter = course.chapters[i] let chapter = course.chapters[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,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

@ -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,6 +1,6 @@
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>
) )

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",
@ -69,6 +70,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",

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)
@ -182,6 +185,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 +969,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 +1053,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:
@ -2866,6 +2898,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 +3911,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 +4829,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 +4905,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)
@ -6979,6 +7061,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 +8179,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: {}