Skip to content

Commit

Permalink
feat: ability to construct a tutorial (#113)
Browse files Browse the repository at this point in the history
* feat: ability to construct a tutorial

* add changesets
  • Loading branch information
joelhooks committed Mar 21, 2024
1 parent 01a4546 commit c22006c
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 155 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-chicken-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coursebuilder/core": patch
---

adds the ability to create a tutorial collection, which is a flat playlist (no sections yet)
19 changes: 18 additions & 1 deletion apps/course-builder-web/src/app/tutorials/[module]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import * as React from 'react'
import { notFound, redirect } from 'next/navigation'
import { getAbility } from '@/ability'
import ModuleEdit from '@/components/module-edit'
import { db } from '@/db'
import { contentResource, contentResourceResource } from '@/db/schema'
import { TipSchema } from '@/lib/tips'
import { getServerAuthSession } from '@/server/auth'
import { asc, like, sql } from 'drizzle-orm'
import { last } from 'lodash'

export const dynamic = 'force-dynamic'

Expand All @@ -14,12 +19,24 @@ export default async function EditTutorialPage({ params }: { params: { module: s
redirect('/login')
}

const tutorial = null
const tutorial = await db.query.contentResource.findFirst({
where: like(contentResource.id, `%${last(params.module.split('-'))}%`),
with: {
resources: {
with: {
resource: true,
},
orderBy: asc(contentResourceResource.position),
},
},
})

if (!tutorial) {
notFound()
}

console.log(`page load`, { tutorial })

return (
<>
<ModuleEdit tutorial={tutorial} />
Expand Down
4 changes: 3 additions & 1 deletion apps/course-builder-web/src/app/tutorials/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import { notFound } from 'next/navigation'
import { getAbility } from '@/ability'
import CreateResourcePage from '@/components/resources-crud/create-resource-page'
import { getServerAuthSession } from '@/server/auth'

export const dynamic = 'force-dynamic'
Expand All @@ -15,7 +16,8 @@ export default async function NewTutorialPage() {

return (
<div className="flex flex-col">
<div>New Tutorial Form</div>
<h1 className="text-3xl font-bold sm:text-4xl">Create a New Tutorial</h1>
<CreateResourcePage resourceType="tutorial" />
</div>
)
}
11 changes: 9 additions & 2 deletions apps/course-builder-web/src/app/tutorials/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Link from 'next/link'
import { getAbility } from '@/ability'
import { getServerAuthSession } from '@/server/auth'

import { Card, CardContent, CardHeader, CardTitle } from '@coursebuilder/ui'
import { Button, Card, CardContent, CardHeader, CardTitle } from '@coursebuilder/ui'

export default async function Tutorials() {
const session = await getServerAuthSession()
Expand All @@ -12,7 +12,14 @@ export default async function Tutorials() {

return (
<div className="flex flex-col">
{ability.can('create', 'Content') ? <Link href="/tutorials/new">New Tutorial</Link> : null}
{ability.can('update', 'Content') ? (
<div className="bg-muted flex h-9 w-full items-center justify-between px-1">
<div />
<Button asChild className="h-7">
<Link href={`/tutorials/new`}>New Tutorial</Link>
</Button>
</div>
) : null}
{tutorials.map((tutorial) => (
<Link href={`/tutorials/${tutorial.slug}`} key={tutorial._id}>
<Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
import invariant from 'tiny-invariant'

import { ContentResource } from '@coursebuilder/core/types'

export type TreeItem = {
id: string
label?: string
isDraft?: boolean
type?: string
children: TreeItem[]
isOpen?: boolean
itemData?: Record<string, unknown>
itemData?: { position: number; resource: ContentResource; resourceId: string; resourceOfId: string }
}

export type TreeState = {
Expand Down
25 changes: 22 additions & 3 deletions apps/course-builder-web/src/components/lesson-list/tree.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import { courseBuilderConfig } from '@/coursebuilder/course-builder-config'
import { updateResourcePosition } from '@/lib/tutorials-query'
import { triggerPostMoveFlash } from '@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash'
import { Instruction, ItemMode } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
import * as liveRegion from '@atlaskit/pragmatic-drag-and-drop-live-region'
Expand Down Expand Up @@ -38,6 +39,7 @@ function createTreeItemRegistry() {

export default function Tree({ initialData }: { initialData?: TreeItemType[] }) {
const [state, updateState] = useReducer(treeStateReducer, initialData, getInitialTreeState)
const params = useParams<{ module: string }>()

const ref = useRef<HTMLDivElement>(null)
const { extractInstruction } = useContext(DependencyContext)
Expand All @@ -50,11 +52,28 @@ export default function Tree({ initialData }: { initialData?: TreeItemType[] })
lastStateRef.current = data
}, [data])

const saveTreeData = useCallback(async () => {
const currentData = lastStateRef.current
console.log('currentData', currentData)
for (const item of currentData) {
if (!item.itemData) continue
await updateResourcePosition({
tutorialId: params.module as string,
resourceId: item.itemData.resourceId,
position: currentData.indexOf(item),
})
}
}, [params])

useEffect(() => {
if (lastAction === null) {
return
}

saveTreeData()

console.log('lastAction', lastAction)

if (lastAction.type === 'modal-move') {
const parentName = lastAction.targetId === '' ? 'the root' : `Item ${lastAction.targetId}`

Expand Down Expand Up @@ -84,7 +103,7 @@ export default function Tree({ initialData }: { initialData?: TreeItemType[] })

return
}
}, [lastAction, registry])
}, [lastAction, registry, saveTreeData])

useEffect(() => {
return () => {
Expand Down
167 changes: 35 additions & 132 deletions apps/course-builder-web/src/components/module-edit.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,43 @@
/**
* v0 by Vercel.
* @see https://v0.dev/t/Hw1J8tftoVR
*/
'use client'

import * as React from 'react'
import { useRouter } from 'next/navigation'
import { EditTutorialForm } from '@/app/tutorials/[module]/edit/_form'
import Tree from '@/components/lesson-list/tree'
import { CreateResourceForm } from '@/components/resources-crud/create-resource-form'
import { db } from '@/db'
import { contentResourceResource } from '@/db/schema'
import { addResourceToTutorial } from '@/lib/tutorials-query'

import { ContentResource } from '@coursebuilder/core/types'
import { Button, Input, Label, Textarea } from '@coursebuilder/ui'

export default function Component({ tutorial }: { tutorial: any }) {
export default function Component({
tutorial,
}: {
tutorial: ContentResource & {
resources: { position: number; resource: ContentResource; resourceId: string; resourceOfId: string }[]
}
}) {
const router = useRouter()
return (
<div key="1" className="grid grid-cols-8 gap-4 p-4">
<div className="col-span-2">
<h1 className="text-2xl font-bold">This is my Product</h1>
<p className="my-2 text-sm">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</p>
<EditTutorialForm moduleSlug={tutorial.slug} />
<h1 className="text-2xl font-bold">{tutorial.fields?.title}</h1>
{tutorial.fields?.description && <p className="my-2 text-sm">{tutorial.fields?.description}</p>}
<div className="space-y-2">
<div>
<h2 className="font-bold">Section 1</h2>
<ul className="list-disc pl-4">
<li>Lesson 1</li>
<li>Lesson 2</li>
<li>Generate Titles</li>
</ul>
</div>
<div>
<h2 className="font-bold">Section 2</h2>
<ul className="list-disc pl-4">
<li>Lesson 3</li>
</ul>
</div>
<div>
<h2 className="font-bold">External Resource</h2>
<ul className="list-disc pl-4">
<li>Generate Embeddings</li>
</ul>
</div>
<CreateResourceForm
resourceType={'lesson'}
onCreate={async (resource) => {
await addResourceToTutorial({
resource,
tutorialId: tutorial.id,
})
router.refresh()
}}
/>
<Button className="mt-2" variant="outline">
+ add lessons
+ add a lesson
</Button>
<Button className="mt-2" variant="outline">
+ add section
Expand All @@ -55,114 +50,22 @@ export default function Component({ tutorial }: { tutorial: any }) {
sss
<Tree
initialData={[
...(tutorial.sections
? tutorial.sections.map((section: any) => {
return {
id: section._id,
label: section.title,
type: section.moduleType,
itemData: section,
children:
section.lessons?.map((lesson: any) => {
return {
id: lesson._id,
label: lesson.title,
type: lesson.moduleType,
children: [],
itemData: lesson,
}
}) ?? [],
}
})
: []),
...(tutorial.lessons
? tutorial.lessons.map((lesson: any) => {
...(tutorial.resources
? tutorial.resources.map((resourceItem) => {
console.log(resourceItem)
return {
id: lesson._id,
label: lesson.title,
type: lesson.moduleType,
id: resourceItem.resource.id,
label: resourceItem.resource.fields?.title || 'lessonzzz',
type: resourceItem.resource.type,
children: [],
itemData: lesson,
itemData: resourceItem,
}
})
: []),
]}
/>
</div>
</div>
<div className="col-span-4">
<h2 className="text-xl font-bold">Lesson 2</h2>
<div className="mt-2 aspect-video bg-gray-200" />
<div className="mt-4 flex items-center justify-between">
<h2 className="text-xl font-bold">Lesson 2 - Problem</h2>
<div className="flex space-x-2">
<Button variant="ghost">body</Button>
<Button variant="ghost">transcript</Button>
<Button variant="ghost">
<ExpandIcon className="h-4 w-4" />
</Button>
</div>
</div>
<div className="mt-4">
<Label htmlFor="markdown-editor">Markdown Editor</Label>
<Textarea id="markdown-editor" placeholder="Type your markdown here." />
<p className="text-sm text-gray-500 dark:text-gray-400">This editor supports markdown syntax.</p>
</div>
<form className="mt-4 space-y-4">
<section className="mt-4 space-y-4">
<h2 className="text-xl font-bold">Exercise</h2>
<textarea className="mt-2 h-[100px] w-full border border-gray-300 p-2" placeholder="Exercise Body Text" />
</section>
<h2 className="text-xl font-bold">Solution Video URL</h2>
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="solutionVideo">Upload Solution Video</Label>
<Input id="solutionVideo" type="file" />
</div>
<textarea className="mt-2 h-[100px] w-full border border-gray-300 p-2" placeholder="Solution Body Text" />
<section className="mt-4 space-y-4">
<h2 className="text-xl font-bold">Resources</h2>
<ul className="list-disc pl-4">
<li>Resource 1</li>
<li>Resource 2</li>
<li>Resource 3</li>
</ul>
<Button variant="outline">+ add resource</Button>
</section>
</form>
</div>
<div className="col-span-2 rounded-lg bg-gray-100 p-4">
<h2 className="mb-4 text-xl font-bold">Chat Assistant</h2>
<div className="mb-4 h-[300px] overflow-auto rounded-md border border-gray-200">
<div className="p-4 text-gray-500">Start a conversation with the assistant...</div>
</div>
<div>
<Label htmlFor="chat-input">New Message</Label>
<Textarea className="mb-2" id="chat-input" placeholder="Type your message here." />
<Button>Send</Button>
</div>
</div>
</div>
)
}

function ExpandIcon(props: any) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
<path d="M3 7.8V3m0 0h4.8M3 3l6 6" />
</svg>
)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { CreateResourceForm } from '@/components/resources-crud/create-resource-form'

import { Card, CardContent, CardFooter, CardHeader } from '@coursebuilder/ui'
Expand Down
Loading

0 comments on commit c22006c

Please sign in to comment.