diff --git a/apps/egghead/package.json b/apps/egghead/package.json
index 72ad4d3b8..2c19ad9fa 100644
--- a/apps/egghead/package.json
+++ b/apps/egghead/package.json
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "concurrently --kill-others-on-fail \"npm:dev:*\"",
- "dev:next": "next dev",
+ "dev:next": "NODE_OPTIONS='--inspect' next dev --turbo",
"dev:inngest": "pnpx inngest-cli@latest dev --no-discovery -u http://localhost:3000/api/inngest",
"dev:party": "pnpx partykit dev",
"tunnel": "ngrok http --domain=neatly-diverse-goldfish.ngrok-free.app 3000",
@@ -21,6 +21,11 @@
"coupons": "dotenv tsx src/scripts/bootstrap-basic-coupons.ts"
},
"dependencies": {
+ "@atlaskit/pragmatic-drag-and-drop": "^1.1.3",
+ "@atlaskit/pragmatic-drag-and-drop-flourish": "^1.0.4",
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
+ "@atlaskit/pragmatic-drag-and-drop-live-region": "^1.0.3",
+ "@atlaskit/tokens": "^1.42.1",
"@auth/core": "^0.37.2",
"@aws-sdk/client-s3": "^3.525.0",
"@aws-sdk/s3-request-presigner": "^3.441.0",
@@ -72,6 +77,7 @@
"inngest": "^3.22.5",
"lodash": "^4.17.21",
"lucide-react": "^0.288.0",
+ "memoize-one": "^6.0.0",
"nanoid": "^5.0.2",
"next": "15.0.3",
"next-auth": "5.0.0-beta.25",
@@ -99,6 +105,8 @@
"tailwind-merge": "^1.14.0",
"tailwind-scrollbar": "^3.0.0",
"tailwindcss-animate": "^1.0.7",
+ "tailwindcss-radix": "^2.8.0",
+ "tiny-invariant": "^1.3.1",
"typesense": "^1.8.2",
"uploadthing": "6.13.2",
"uuid": "^9.0.1",
@@ -112,6 +120,7 @@
"@types/pg": "^8.11.6",
"@types/pluralize": "^0.0.33",
"@types/react": "npm:types-react@19.0.0-rc.1",
+ "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/react-gravatar": "^2.6.13",
"concurrently": "^8.2.2",
"dotenv-cli": "^7.3.0",
diff --git a/apps/egghead/src/app/(content)/posts/[slug]/edit/page.tsx b/apps/egghead/src/app/(content)/posts/[slug]/edit/page.tsx
index 3c2a88dd1..c23e6a408 100644
--- a/apps/egghead/src/app/(content)/posts/[slug]/edit/page.tsx
+++ b/apps/egghead/src/app/(content)/posts/[slug]/edit/page.tsx
@@ -9,6 +9,26 @@ import { subject } from '@casl/ability'
import { EditPostForm } from '../../_components/edit-post-form'
+function EditPostSkeleton() {
+ return (
+
+ )
+}
+
export const dynamic = 'force-dynamic'
export default async function PostPage(props: {
@@ -40,15 +60,19 @@ export default async function PostPage(props: {
await getAllEggheadTagsCached()
const tagLoader = getTags()
+ console.log({ videoResourceLoader, tagLoader, post })
+
return (
-
+ }>
+
+
)
}
diff --git a/apps/egghead/src/app/(content)/posts/_components/create-post.tsx b/apps/egghead/src/app/(content)/posts/_components/create-post.tsx
index b9b0aee6e..1f10ef550 100644
--- a/apps/egghead/src/app/(content)/posts/_components/create-post.tsx
+++ b/apps/egghead/src/app/(content)/posts/_components/create-post.tsx
@@ -2,6 +2,8 @@
import { useRouter } from 'next/navigation'
import { PostUploader } from '@/app/(content)/posts/_components/post-uploader'
+import { NewResourceWithVideoForm } from '@/components/resources-crud/new-resource-with-video-form'
+import { PostSchema } from '@/lib/posts'
import { createPost } from '@/lib/posts-query'
import { getVideoResource } from '@/lib/video-resource-query'
import { signOut } from 'next-auth/react'
@@ -12,7 +14,6 @@ import {
type ContentResource,
} from '@coursebuilder/core/schemas'
import { Card, CardContent, CardFooter, CardHeader } from '@coursebuilder/ui'
-import { NewResourceWithVideoForm } from '@coursebuilder/ui/resources-crud/new-resource-with-video-form'
export function CreatePost() {
const router = useRouter()
@@ -25,12 +26,13 @@ export function CreatePost() {
`/${pluralize(resource.type)}/${resource.fields?.slug || resource.id}/edit`,
)
}}
- createResource={async ({ title, videoResourceId }) => {
+ createResource={async ({ title, videoResourceId, postType }) => {
try {
return ContentResourceSchema.parse(
await createPost({
title,
videoResourceId,
+ postType,
}),
)
} catch (error) {
@@ -41,6 +43,7 @@ export function CreatePost() {
}
}}
getVideoResource={getVideoResource}
+ availableResourceTypes={['lesson', 'article', 'podcast', 'course']}
>
{(handleSetVideoResourceId: (id: string) => void) => {
return (
diff --git a/apps/egghead/src/app/(content)/posts/_components/edit-post-form-metadata.tsx b/apps/egghead/src/app/(content)/posts/_components/edit-post-form-metadata.tsx
index 7a84014ff..4b1eec1c6 100644
--- a/apps/egghead/src/app/(content)/posts/_components/edit-post-form-metadata.tsx
+++ b/apps/egghead/src/app/(content)/posts/_components/edit-post-form-metadata.tsx
@@ -7,7 +7,12 @@ import { NewLessonVideoForm } from '@/components/resources-crud/new-lesson-video
import AdvancedTagSelector from '@/components/resources-crud/tag-selector'
import { env } from '@/env.mjs'
import { useTranscript } from '@/hooks/use-transcript'
-import { Post, PostSchema, PostTypeSchema } from '@/lib/posts'
+import {
+ Post,
+ POST_TYPES_WITH_VIDEO,
+ PostSchema,
+ PostTypeSchema,
+} from '@/lib/posts'
import { addTagToPost, removeTagFromPost } from '@/lib/posts-query'
import { EggheadTag } from '@/lib/tags'
import { api } from '@/trpc/react'
@@ -109,76 +114,78 @@ export const PostMetadataFormFields: React.FC<{
})
return (
<>
-
-
-
- video is loading
-
- >
- }
- >
- {videoResourceId ? (
- replacingVideo ? (
-
- {
- setReplacingVideo(false)
- setVideoUploadStatus('finalizing upload')
- setVideoResourceId(videoResourceId)
- }}
- onVideoResourceCreated={(videoResourceId) =>
- setVideoResourceId(videoResourceId)
- }
- />
- setReplacingVideo(false)}
- >
- Cancel Replace Video
-
-
- ) : (
+ {POST_TYPES_WITH_VIDEO.includes(post.fields.postType) && (
+
+
- {videoResource && videoResource.state === 'ready' ? (
-
-
-
setReplacingVideo(true)}
- >
- Replace Video
-
-
- ) : videoResource ? (
-
- video is {videoResource.state}
-
- ) : (
-
- video is {videoUploadStatus}
-
- )}
+
+ video is loading
+
>
- )
- ) : (
- {
- setVideoUploadStatus('finalizing upload')
- setVideoResourceId(videoResourceId)
- }}
- onVideoResourceCreated={(videoResourceId) =>
- setVideoResourceId(videoResourceId)
- }
- />
- )}
-
-
+ }
+ >
+ {videoResourceId ? (
+ replacingVideo ? (
+
+ {
+ setReplacingVideo(false)
+ setVideoUploadStatus('finalizing upload')
+ setVideoResourceId(videoResourceId)
+ }}
+ onVideoResourceCreated={(videoResourceId) =>
+ setVideoResourceId(videoResourceId)
+ }
+ />
+ setReplacingVideo(false)}
+ >
+ Cancel Replace Video
+
+
+ ) : (
+ <>
+ {videoResource && videoResource.state === 'ready' ? (
+
+
+
setReplacingVideo(true)}
+ >
+ Replace Video
+
+
+ ) : videoResource ? (
+
+ video is {videoResource.state}
+
+ ) : (
+
+ video is {videoUploadStatus}
+
+ )}
+ >
+ )
+ ) : (
+ {
+ setVideoUploadStatus('finalizing upload')
+ setVideoResourceId(videoResourceId)
+ }}
+ onVideoResourceCreated={(videoResourceId) =>
+ setVideoResourceId(videoResourceId)
+ }
+ />
+ )}
+
+
+ )}
(
+
+ ),
+ toolComponent: (
+
+
+
+ ),
+ },
{
id: 'publish-checklist',
label: 'Publish Checklist',
@@ -104,7 +122,6 @@ export function EditPostForm({
},
{ id: 'assistant' },
]}
- theme={theme}
>
{
+ const newPath = showingAll ? '/posts' : '/posts?view=all'
+ router.push(newPath)
+ }
+
+ return (
+
+ {showingAll ? '👤 Show My Posts' : '👥 Show All Posts'}
+
+ )
+}
diff --git a/apps/egghead/src/app/(content)/posts/_components/resource-resources-list.tsx b/apps/egghead/src/app/(content)/posts/_components/resource-resources-list.tsx
new file mode 100644
index 000000000..3fe99962d
--- /dev/null
+++ b/apps/egghead/src/app/(content)/posts/_components/resource-resources-list.tsx
@@ -0,0 +1,157 @@
+'use client'
+
+import * as React from 'react'
+import { useReducer } from 'react'
+import { useRouter } from 'next/navigation'
+import {
+ getInitialTreeState,
+ treeStateReducer,
+} from '@/components/lesson-list/data/tree'
+import Tree from '@/components/lesson-list/tree'
+import { CreatePostForm } from '@/components/resources-crud/create-post-form'
+import { SearchExistingLessons } from '@/components/resources-crud/search-existing-lessons'
+import { addResourceToResource, createPost } from '@/lib/posts-query'
+import { createResource } from '@/lib/resources/create-resources'
+
+import type { ContentResource } from '@coursebuilder/core/schemas'
+import { Button } from '@coursebuilder/ui'
+import { CreateResourceForm } from '@coursebuilder/ui/resources-crud/create-resource-form'
+
+type FormState = {
+ activeForm: 'lesson' | 'section' | 'existing_lesson' | null
+}
+
+type FormAction =
+ | { type: 'SHOW_LESSON_FORM' }
+ | { type: 'SHOW_SECTION_FORM' }
+ | { type: 'SHOW_EXISTING_LESSON_FORM' }
+ | { type: 'HIDE_FORM' }
+
+function formReducer(state: FormState, action: FormAction): FormState {
+ switch (action.type) {
+ case 'SHOW_LESSON_FORM':
+ return { activeForm: 'lesson' }
+ case 'SHOW_SECTION_FORM':
+ return { activeForm: 'section' }
+ case 'SHOW_EXISTING_LESSON_FORM':
+ return { activeForm: 'existing_lesson' }
+ case 'HIDE_FORM':
+ return { activeForm: null }
+ default:
+ return state
+ }
+}
+
+export function ResourceResourcesList({
+ resource,
+}: {
+ resource: ContentResource
+}) {
+ const [formState, formDispatch] = useReducer(formReducer, {
+ activeForm: null,
+ })
+
+ const initialData = [
+ ...(resource.resources
+ ? resource.resources.map((resourceItem) => {
+ if (!resourceItem.resource) {
+ throw new Error('resourceItem.resource is required')
+ }
+ const resources = resourceItem.resource.resources ?? []
+ return {
+ id: resourceItem.resource.id,
+ label: resourceItem.resource.fields?.title,
+ type: resourceItem.resource.type,
+ children: resources.map((resourceItem: any) => {
+ if (!resourceItem.resource) {
+ throw new Error('resourceItem.resource is required')
+ }
+ return {
+ id: resourceItem.resource.id,
+ label: resourceItem.resource.fields?.title,
+ type: resourceItem.resource.type,
+ children: [],
+ itemData: resourceItem as any,
+ }
+ }),
+ itemData: resourceItem as any,
+ }
+ })
+ : []),
+ ]
+ const [state, updateState] = useReducer(
+ treeStateReducer,
+ initialData,
+ getInitialTreeState,
+ )
+ const router = useRouter()
+
+ const handleResourceCreated = async (resource: ContentResource) => {
+ const resourceItem = await addResourceToResource({
+ resource,
+ resourceId: resource.id,
+ })
+
+ if (resourceItem) {
+ updateState({
+ type: 'add-item',
+ itemId: resourceItem.resource.id,
+ item: {
+ id: resourceItem.resource.id,
+ label: resourceItem.resource.fields?.title,
+ type: resourceItem.resource.type,
+ children: [],
+ itemData: resourceItem as any,
+ },
+ })
+ }
+
+ formDispatch({ type: 'HIDE_FORM' })
+ router.refresh()
+ }
+
+ return (
+ <>
+ Resources
+
+
+ {formState.activeForm === 'lesson' && (
+
formDispatch({ type: 'HIDE_FORM' })}
+ />
+ )}
+ {formState.activeForm === 'existing_lesson' && (
+ formDispatch({ type: 'HIDE_FORM' })}
+ />
+ )}
+
+ formDispatch({ type: 'SHOW_LESSON_FORM' })}
+ className="mt-2"
+ variant="outline"
+ >
+ + add a lesson
+
+ formDispatch({ type: 'SHOW_EXISTING_LESSON_FORM' })}
+ className="mt-2"
+ variant="outline"
+ >
+ + add existing lesson
+
+
+
+ >
+ )
+}
diff --git a/apps/egghead/src/app/(content)/posts/page.tsx b/apps/egghead/src/app/(content)/posts/page.tsx
index 9371c8368..81e0836db 100644
--- a/apps/egghead/src/app/(content)/posts/page.tsx
+++ b/apps/egghead/src/app/(content)/posts/page.tsx
@@ -1,6 +1,7 @@
import * as React from 'react'
import { Suspense } from 'react'
import Link from 'next/link'
+import { redirect } from 'next/navigation'
import { CreatePost } from '@/app/(content)/posts/_components/create-post'
import { DeletePostButton } from '@/app/(content)/posts/_components/delete-post-button'
import {
@@ -24,15 +25,26 @@ import {
CardFooter,
CardHeader,
CardTitle,
+ Checkbox,
} from '@coursebuilder/ui'
-export default async function PostsListPage() {
+import { PostsFilterToggle } from './_components/posts-filter-toggle'
+
+export default async function PostsListPage(props: {
+ searchParams: Promise<{ [key: string]: string | undefined }>
+}) {
+ const searchParams = await props.searchParams
+ const { ability } = await getServerAuthSession()
+
return (
@@ -42,12 +54,12 @@ export default async function PostsListPage() {
)
}
-async function PostList() {
+async function PostList({ showAllPosts }: { showAllPosts: boolean }) {
const { ability, session } = await getServerAuthSession()
let postsModule
- if (ability.can('manage', 'all')) {
+ if (ability.can('manage', 'all') && showAllPosts) {
postsModule = await getAllPosts()
} else {
postsModule = await getAllPostsForUser(session?.user?.id)
@@ -60,8 +72,10 @@ async function PostList() {
return (
-
- {post.fields?.state}
+
+
+ {post.fields?.state} {post.fields?.postType}
+
{/* posts are presented at the root of the site and not in a sub-route */}
@@ -103,10 +117,9 @@ async function PostList() {
const InstructorByLine = async ({ userId }: { userId: string }) => {
const instructor = await getCachedEggheadInstructorForUser(userId)
- return instructor ? (
-
- {instructor?.first_name} {instructor?.last_name}
-
+ const fullName = `${instructor?.first_name} ${instructor?.last_name}`.trim()
+ return Boolean(fullName) ? (
+ {fullName}
) : null
}
diff --git a/apps/egghead/src/components/lesson-list/data/tree.ts b/apps/egghead/src/components/lesson-list/data/tree.ts
new file mode 100644
index 000000000..ae664cfb0
--- /dev/null
+++ b/apps/egghead/src/components/lesson-list/data/tree.ts
@@ -0,0 +1,376 @@
+import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
+import invariant from 'tiny-invariant'
+
+import { ContentResource } from '@coursebuilder/core/schemas'
+
+export type TreeItem = {
+ id: string
+ label?: string
+ isDraft?: boolean
+ type?: string
+ children: TreeItem[]
+ isOpen?: boolean
+ itemData?: {
+ position: number
+ resource: ContentResource
+ resourceId: string
+ resourceOfId: string
+ }
+}
+
+export type TreeState = {
+ lastAction: TreeAction | null
+ data: TreeItem[]
+}
+
+export function getInitialTreeState(initialData?: TreeItem[]): TreeState {
+ return { data: initialData ?? getInitialData(), lastAction: null }
+}
+
+export function getInitialData(): TreeItem[] {
+ return [
+ {
+ id: '1',
+ isOpen: true,
+
+ children: [
+ {
+ id: '1.3',
+ isOpen: true,
+
+ children: [
+ {
+ id: '1.3.1',
+ children: [],
+ },
+ {
+ id: '1.3.2',
+ isDraft: true,
+ children: [],
+ },
+ ],
+ },
+ { id: '1.4', children: [] },
+ ],
+ },
+ {
+ id: '2',
+ isOpen: true,
+ children: [
+ {
+ id: '2.3',
+ isOpen: true,
+
+ children: [
+ {
+ id: '2.3.1',
+ children: [],
+ },
+ {
+ id: '2.3.2',
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]
+}
+
+export type TreeAction =
+ | {
+ type: 'instruction'
+ instruction: Instruction
+ itemId: string
+ targetId: string
+ }
+ | {
+ type: 'toggle'
+ itemId: string
+ }
+ | {
+ type: 'add-item'
+ item: TreeItem
+ itemId: string
+ }
+ | {
+ type: 'expand'
+ itemId: string
+ }
+ | {
+ type: 'collapse'
+ itemId: string
+ }
+ | { type: 'modal-move'; itemId: string; targetId: string; index: number }
+ | { type: 'remove-item'; itemId: string }
+
+export const tree = {
+ remove(data: TreeItem[], id: string): TreeItem[] {
+ return data
+ .filter((item) => item.id !== id)
+ .map((item) => {
+ if (tree.hasChildren(item)) {
+ return {
+ ...item,
+ children: tree.remove(item.children, id),
+ }
+ }
+ return item
+ })
+ },
+ insertBefore(
+ data: TreeItem[],
+ targetId: string,
+ newItem: TreeItem,
+ ): TreeItem[] {
+ return data.flatMap((item) => {
+ if (item.id === targetId) {
+ return [newItem, item]
+ }
+ if (tree.hasChildren(item)) {
+ return {
+ ...item,
+ children: tree.insertBefore(item.children, targetId, newItem),
+ }
+ }
+ return item
+ })
+ },
+ insertAfter(
+ data: TreeItem[],
+ targetId: string,
+ newItem: TreeItem,
+ ): TreeItem[] {
+ return data.flatMap((item) => {
+ if (item.id === targetId) {
+ return [item, newItem]
+ }
+
+ if (tree.hasChildren(item)) {
+ return {
+ ...item,
+ children: tree.insertAfter(item.children, targetId, newItem),
+ }
+ }
+
+ return item
+ })
+ },
+ insertChild(
+ data: TreeItem[],
+ targetId: string,
+ newItem: TreeItem,
+ ): TreeItem[] {
+ return data.flatMap((item) => {
+ if (item.id === targetId) {
+ // already a parent: add as first child
+ return {
+ ...item,
+ // opening item so you can see where item landed
+ isOpen: true,
+ children: [newItem, ...item.children],
+ }
+ }
+
+ if (!tree.hasChildren(item)) {
+ return item
+ }
+
+ return {
+ ...item,
+ children: tree.insertChild(item.children, targetId, newItem),
+ }
+ })
+ },
+ find(data: TreeItem[], itemId: string): TreeItem | undefined {
+ for (const item of data) {
+ if (item.id === itemId) {
+ return item
+ }
+
+ if (tree.hasChildren(item)) {
+ const result = tree.find(item.children, itemId)
+ if (result) {
+ return result
+ }
+ }
+ }
+ },
+ getPathToItem({
+ current,
+ targetId,
+ parentIds = [],
+ }: {
+ current: TreeItem[]
+ targetId: string
+ parentIds?: string[]
+ }): string[] | undefined {
+ for (const item of current) {
+ if (item.id === targetId) {
+ return parentIds
+ }
+ const nested = tree.getPathToItem({
+ current: item.children,
+ targetId: targetId,
+ parentIds: [...parentIds, item.id],
+ })
+ if (nested) {
+ return nested
+ }
+ }
+ },
+ hasChildren(item: TreeItem): boolean {
+ return item.children.length > 0
+ },
+}
+
+export function treeStateReducer(
+ state: TreeState,
+ action: TreeAction,
+): TreeState {
+ return {
+ data: dataReducer(state.data, action),
+ lastAction: action,
+ }
+}
+
+const dataReducer = (data: TreeItem[], action: TreeAction) => {
+ if (action.type === 'add-item') {
+ return [...data, action.item]
+ }
+
+ const item = tree.find(data, action.itemId)
+ if (!item) {
+ return data
+ }
+
+ if (action.type === 'instruction') {
+ const instruction = action.instruction
+
+ if (instruction.type === 'reparent') {
+ const path = tree.getPathToItem({
+ current: data,
+ targetId: action.targetId,
+ })
+ invariant(path)
+ const desiredId = path[instruction.desiredLevel] as string
+ let result = tree.remove(data, action.itemId)
+ result = tree.insertAfter(result, desiredId, item)
+ return result
+ }
+
+ // the rest of the actions require you to drop on something else
+ if (action.itemId === action.targetId) {
+ return data
+ }
+
+ if (instruction.type === 'reorder-above') {
+ let result = tree.remove(data, action.itemId)
+ result = tree.insertBefore(result, action.targetId, item)
+ return result
+ }
+
+ if (instruction.type === 'reorder-below') {
+ let result = tree.remove(data, action.itemId)
+ result = tree.insertAfter(result, action.targetId, item)
+ return result
+ }
+
+ if (instruction.type === 'make-child') {
+ let result = tree.remove(data, action.itemId)
+ result = tree.insertChild(result, action.targetId, item)
+ return result
+ }
+
+ console.warn('TODO: action not implemented', instruction)
+
+ return data
+ }
+
+ function toggle(item: TreeItem): TreeItem {
+ if (!tree.hasChildren(item)) {
+ return item
+ }
+
+ if (item.id === action.itemId) {
+ return { ...item, isOpen: !item.isOpen }
+ }
+
+ return { ...item, children: item.children.map(toggle) }
+ }
+
+ if (action.type === 'toggle') {
+ return data.map(toggle)
+ }
+
+ if (action.type === 'expand') {
+ if (tree.hasChildren(item) && !item.isOpen) {
+ return data.map(toggle)
+ }
+ return data
+ }
+
+ if (action.type === 'collapse') {
+ if (tree.hasChildren(item) && item.isOpen) {
+ return data.map(toggle)
+ }
+ return data
+ }
+
+ if (action.type === 'remove-item') {
+ return tree.remove(data, action.itemId)
+ }
+
+ if (action.type === 'modal-move') {
+ let result = tree.remove(data, item.id)
+
+ const siblingItems = getChildItems(result, action.targetId)
+
+ if (siblingItems.length === 0) {
+ if (action.targetId === '') {
+ /**
+ * If the target is the root level, and there are no siblings, then
+ * the item is the only thing in the root level.
+ */
+ result = [item]
+ } else {
+ /**
+ * Otherwise for deeper levels that have no children, we need to
+ * use `insertChild` instead of inserting relative to a sibling.
+ */
+ result = tree.insertChild(result, action.targetId, item)
+ }
+ } else if (action.index === siblingItems.length) {
+ const relativeTo = siblingItems[siblingItems.length - 1] as TreeItem
+ /**
+ * If the position selected is the end, we insert after the last item.
+ */
+ result = tree.insertAfter(result, relativeTo.id, item)
+ } else {
+ const relativeTo = siblingItems[action.index] as TreeItem
+ /**
+ * Otherwise we insert before the existing item in the given position.
+ * This results in the new item being in that position.
+ */
+ result = tree.insertBefore(result, relativeTo.id, item)
+ }
+
+ return result
+ }
+
+ return data
+}
+
+function getChildItems(data: TreeItem[], targetId: string) {
+ /**
+ * An empty string is representing the root
+ */
+ if (targetId === '') {
+ return data
+ }
+
+ const targetItem = tree.find(data, targetId)
+ invariant(targetItem)
+
+ return targetItem.children
+}
diff --git a/apps/egghead/src/components/lesson-list/pieces/tree/constants.ts b/apps/egghead/src/components/lesson-list/pieces/tree/constants.ts
new file mode 100644
index 000000000..59af80d81
--- /dev/null
+++ b/apps/egghead/src/components/lesson-list/pieces/tree/constants.ts
@@ -0,0 +1 @@
+export const indentPerLevel = 24
diff --git a/apps/egghead/src/components/lesson-list/pieces/tree/tree-context.tsx b/apps/egghead/src/components/lesson-list/pieces/tree/tree-context.tsx
new file mode 100644
index 000000000..b65001e62
--- /dev/null
+++ b/apps/egghead/src/components/lesson-list/pieces/tree/tree-context.tsx
@@ -0,0 +1,54 @@
+import React, { createContext } from 'react'
+import {
+ attachInstruction,
+ extractInstruction,
+} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
+
+import { Product } from '@coursebuilder/core/schemas'
+import type { ContentResource } from '@coursebuilder/core/schemas'
+
+import type { TreeAction, TreeItem } from '../../data/tree'
+
+export type TreeContextValue = {
+ dispatch: (action: TreeAction) => void
+ uniqueContextId: Symbol
+ getPathToItem: (itemId: string) => string[]
+ getMoveTargets: ({ itemId }: { itemId: string }) => TreeItem[]
+ getChildrenOfItem: (itemId: string) => TreeItem[]
+ rootResourceId: string | null
+ rootResource: ContentResource | Product | null
+ registerTreeItem: (args: {
+ itemId: string
+ element: HTMLElement
+ actionMenuTrigger: HTMLElement
+ }) => void
+}
+
+export const TreeContext = createContext({
+ dispatch: () => {},
+ uniqueContextId: Symbol('uniqueId'),
+ getPathToItem: () => [],
+ getMoveTargets: () => [],
+ getChildrenOfItem: () => [],
+ registerTreeItem: () => {},
+ rootResourceId: null,
+ rootResource: null,
+})
+
+export type DependencyContext = {
+ DropIndicator: React.JSX.Element
+ attachInstruction: typeof attachInstruction
+ extractInstruction: typeof extractInstruction
+}
+
+export const DependencyContext = createContext({
+ DropIndicator: (
+
+ â—Ž
+
+ ),
+ attachInstruction: attachInstruction,
+ extractInstruction: extractInstruction,
+})
diff --git a/apps/egghead/src/components/lesson-list/pieces/tree/tree-item.tsx b/apps/egghead/src/components/lesson-list/pieces/tree/tree-item.tsx
new file mode 100644
index 000000000..b2acfbe58
--- /dev/null
+++ b/apps/egghead/src/components/lesson-list/pieces/tree/tree-item.tsx
@@ -0,0 +1,449 @@
+import React, {
+ Fragment,
+ memo,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react'
+import { usePathname, useRouter } from 'next/navigation'
+import { removeSection } from '@/lib/posts-query'
+import { cn } from '@/utils/cn'
+import {
+ Instruction,
+ ItemMode,
+} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
+import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
+import {
+ draggable,
+ dropTargetForElements,
+ monitorForElements,
+} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
+import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview'
+import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
+import type { DragLocationHistory } from '@atlaskit/pragmatic-drag-and-drop/types'
+import { token } from '@atlaskit/tokens'
+import { ChevronDown, ChevronUp, Trash } from 'lucide-react'
+import pluralize from 'pluralize'
+import { createRoot } from 'react-dom/client'
+import invariant from 'tiny-invariant'
+
+import { Button } from '@coursebuilder/ui'
+
+import { TreeItem as TreeItemType } from '../../data/tree'
+import { indentPerLevel } from './constants'
+import { DependencyContext, TreeContext } from './tree-context'
+
+const iconColor = token('color.icon', '#44546F')
+
+function ChildIcon() {
+ return (
+
+
+
+ )
+}
+
+function GroupIcon({ isOpen }: { isOpen: boolean }) {
+ const Icon = isOpen ? :
+ return (
+
+ {Icon}
+
+ )
+}
+
+function Icon({ item }: { item: TreeItemType }) {
+ if (item.type !== 'section') {
+ return null //
+ }
+ return
+}
+
+function Preview({ item }: { item: TreeItemType }) {
+ return (
+ Item {item.id}
+ )
+}
+
+function getParentLevelOfInstruction(instruction: Instruction): number {
+ if (instruction.type === 'instruction-blocked') {
+ return getParentLevelOfInstruction(instruction.desired)
+ }
+ if (instruction.type === 'reparent') {
+ return instruction.desiredLevel - 1
+ }
+ return instruction.currentLevel - 1
+}
+
+function delay({
+ waitMs: timeMs,
+ fn,
+}: {
+ waitMs: number
+ fn: () => void
+}): () => void {
+ let timeoutId: number | null = window.setTimeout(() => {
+ timeoutId = null
+ fn()
+ }, timeMs)
+ return function cancel() {
+ if (timeoutId) {
+ window.clearTimeout(timeoutId)
+ timeoutId = null
+ }
+ }
+}
+
+const TreeItem = memo(function TreeItem({
+ item,
+ mode,
+ level,
+}: {
+ item: TreeItemType
+ mode: ItemMode
+ level: number
+}) {
+ const buttonRef = useRef(null)
+
+ const [state, setState] = useState<
+ 'idle' | 'dragging' | 'preview' | 'parent-of-instruction'
+ >('idle')
+ const [instruction, setInstruction] = useState(null)
+ const cancelExpandRef = useRef<(() => void) | null>(null)
+
+ const { dispatch, uniqueContextId, getPathToItem, rootResource } =
+ useContext(TreeContext)
+ const { DropIndicator, attachInstruction, extractInstruction } =
+ useContext(DependencyContext)
+ const toggleOpen = useCallback(() => {
+ dispatch({ type: 'toggle', itemId: item.id })
+ }, [dispatch, item])
+
+ const cancelExpand = useCallback(() => {
+ cancelExpandRef.current?.()
+ cancelExpandRef.current = null
+ }, [])
+
+ const clearParentOfInstructionState = useCallback(() => {
+ setState((current) =>
+ current === 'parent-of-instruction' ? 'idle' : current,
+ )
+ }, [])
+
+ // When an item has an instruction applied
+ // we are highlighting it's parent item for improved clarity
+ const shouldHighlightParent = useCallback(
+ (location: DragLocationHistory): boolean => {
+ const target = location.current.dropTargets[0]
+
+ if (!target) {
+ return false
+ }
+
+ const instruction = extractInstruction(target.data)
+
+ if (!instruction) {
+ return false
+ }
+
+ const targetId = target.data.id
+ invariant(typeof targetId === 'string')
+
+ const path = getPathToItem(targetId)
+ const parentLevel: number = getParentLevelOfInstruction(instruction)
+ const parentId = path[parentLevel]
+ return parentId === item.id
+ },
+ [getPathToItem, extractInstruction, item],
+ )
+
+ useEffect(() => {
+ invariant(buttonRef.current)
+
+ function updateIsParentOfInstruction({
+ location,
+ }: {
+ location: DragLocationHistory
+ }) {
+ if (shouldHighlightParent(location)) {
+ setState('parent-of-instruction')
+ return
+ }
+ clearParentOfInstructionState()
+ }
+
+ return combine(
+ draggable({
+ element: buttonRef.current,
+ getInitialData: () => ({
+ id: item.id,
+ type: 'tree-item',
+ isOpenOnDragStart: item.isOpen,
+ uniqueContextId,
+ item: item,
+ }),
+ onGenerateDragPreview: ({ nativeSetDragImage }) => {
+ setCustomNativeDragPreview({
+ getOffset: pointerOutsideOfPreview({ x: '16px', y: '8px' }),
+ render: ({ container }) => {
+ const root = createRoot(container)
+ root.render( )
+ return () => root.unmount()
+ },
+ nativeSetDragImage,
+ })
+ },
+ onDragStart: ({ source }) => {
+ setState('dragging')
+ // collapse open items during a drag
+ if (source.data.isOpenOnDragStart) {
+ dispatch({ type: 'collapse', itemId: item.id })
+ }
+ },
+ onDrop: ({ source }) => {
+ setState('idle')
+ if (source.data.isOpenOnDragStart) {
+ dispatch({ type: 'expand', itemId: item.id })
+ }
+ },
+ }),
+ dropTargetForElements({
+ element: buttonRef.current,
+ getData: ({ input, element, source }) => {
+ const data = { id: item.id }
+
+ return attachInstruction(data, {
+ input,
+ element,
+ indentPerLevel,
+ currentLevel: level,
+ mode,
+ block:
+ (source.data.item as any).type === 'section' ||
+ item.type !== 'section'
+ ? ['make-child']
+ : [],
+ })
+ },
+ canDrop: (allData) => {
+ const { source } = allData
+ return (
+ source.data.type === 'tree-item' &&
+ source.data.uniqueContextId === uniqueContextId
+ )
+ },
+
+ getIsSticky: () => true,
+ onDrag: ({ self, source }) => {
+ const instruction = extractInstruction(self.data)
+
+ if (source.data.id !== item.id) {
+ // expand after 500ms if still merging
+ if (
+ instruction?.type === 'make-child' &&
+ item.children.length &&
+ !item.isOpen &&
+ !cancelExpandRef.current
+ ) {
+ cancelExpandRef.current = delay({
+ waitMs: 500,
+ fn: () => dispatch({ type: 'expand', itemId: item.id }),
+ })
+ }
+ if (instruction?.type !== 'make-child' && cancelExpandRef.current) {
+ cancelExpand()
+ }
+
+ setInstruction(instruction)
+ return
+ }
+ if (instruction?.type === 'reparent') {
+ setInstruction(instruction)
+ return
+ }
+ setInstruction(null)
+ },
+ onDragLeave: () => {
+ cancelExpand()
+ setInstruction(null)
+ },
+ onDrop: () => {
+ cancelExpand()
+ setInstruction(null)
+ },
+ }),
+ monitorForElements({
+ canMonitor: ({ source }) =>
+ source.data.uniqueContextId === uniqueContextId,
+ onDragStart: updateIsParentOfInstruction,
+ onDrag: updateIsParentOfInstruction,
+ onDrop() {
+ clearParentOfInstructionState()
+ },
+ }),
+ )
+ }, [
+ dispatch,
+ item,
+ mode,
+ level,
+ cancelExpand,
+ uniqueContextId,
+ extractInstruction,
+ attachInstruction,
+ getPathToItem,
+ clearParentOfInstructionState,
+ shouldHighlightParent,
+ ])
+
+ useEffect(
+ function mount() {
+ return function unmount() {
+ cancelExpand()
+ }
+ },
+ [cancelExpand],
+ )
+
+ const aria = (() => {
+ // if there are no children, we don't need to set aria attributes
+
+ if (!item.children.length) {
+ return undefined
+ }
+
+ return {
+ 'aria-expanded': item.isOpen,
+ 'aria-controls': `tree-item-${item.id}--subtree`,
+ }
+ })()
+
+ const router = useRouter()
+ const pathname = usePathname()
+
+ return (
+
+
+
{
+ if (item.type === 'event') {
+ router.push(`/events/${item.id}/edit`)
+ return
+ }
+ if (rootResource) {
+ router.push(`/posts/${item.id}/edit`)
+ }
+ }
+ }
+ ref={buttonRef}
+ type="button"
+ style={{ paddingLeft: level * indentPerLevel }}
+ >
+
+
+
+
+ {item.label ?? item.id}
+
+
+
+ {item.type ? (
+
+ {item.type}
+
+ ) : null}
+ {/* ({mode}) */}
+
+
+ {instruction ? (
+
+ ) : null}
+ {/* {instruction ? (
+
+ â—Ž {instruction.type}
+
+ ) : null} */}
+
+ {item.type === 'section' && item.children.length === 0 && (
+
{
+ await removeSection(item.id, pathname)
+ return dispatch({ type: 'remove-item', itemId: item.id })
+ }}
+ >
+
+
+ )}
+
+ {item.children.length && item.isOpen ? (
+
+ {item.children.map((child, index, array) => {
+ const childType: ItemMode = (() => {
+ if (child.children.length && child.isOpen) {
+ return 'expanded'
+ }
+
+ if (index === array.length - 1) {
+ return 'last-in-group'
+ }
+
+ return 'standard'
+ })()
+ return (
+
+ )
+ })}
+
+ ) : null}
+
+ )
+})
+
+export default TreeItem
diff --git a/apps/egghead/src/components/lesson-list/tree.tsx b/apps/egghead/src/components/lesson-list/tree.tsx
new file mode 100644
index 000000000..9bfe46649
--- /dev/null
+++ b/apps/egghead/src/components/lesson-list/tree.tsx
@@ -0,0 +1,317 @@
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import { useParams } from 'next/navigation'
+import { updateResourcePositions } from '@/lib/posts-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'
+import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
+import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
+import memoizeOne from 'memoize-one'
+import invariant from 'tiny-invariant'
+
+import { Product } from '@coursebuilder/core/schemas'
+import type { ContentResource } from '@coursebuilder/core/schemas'
+
+import {
+ tree,
+ TreeAction,
+ TreeItem as TreeItemType,
+ TreeState,
+} from './data/tree'
+import {
+ DependencyContext,
+ TreeContext,
+ TreeContextValue,
+} from './pieces/tree/tree-context'
+import TreeItem from './pieces/tree/tree-item'
+
+type CleanupFn = () => void
+
+function createTreeItemRegistry() {
+ const registry = new Map<
+ string,
+ { element: HTMLElement; actionMenuTrigger: HTMLElement }
+ >()
+
+ const registerTreeItem = ({
+ itemId,
+ element,
+ actionMenuTrigger,
+ }: {
+ itemId: string
+ element: HTMLElement
+ actionMenuTrigger: HTMLElement
+ }): CleanupFn => {
+ registry.set(itemId, { element, actionMenuTrigger })
+ return () => {
+ registry.delete(itemId)
+ }
+ }
+
+ return { registry, registerTreeItem }
+}
+
+export default function Tree({
+ state,
+ updateState,
+ rootResourceId,
+ rootResource,
+}: {
+ state: TreeState
+ updateState: React.Dispatch
+ rootResourceId: string
+ rootResource: ContentResource | Product
+}) {
+ const params = useParams<{ module: string }>()
+
+ const ref = useRef(null)
+ const { extractInstruction } = useContext(DependencyContext)
+
+ const [{ registry, registerTreeItem }] = useState(createTreeItemRegistry)
+
+ const { data, lastAction } = state
+ let lastStateRef = useRef(data)
+ useEffect(() => {
+ lastStateRef.current = data
+ }, [data])
+
+ const saveTreeData = useCallback(async () => {
+ const currentData = lastStateRef.current
+
+ const resourcePositions = currentData.flatMap((item, index) => {
+ if (!item.itemData) return []
+
+ const children = item.children.flatMap((childItem, childIndex) => {
+ if (!childItem.itemData) return []
+
+ return {
+ currentParentResourceId: childItem.itemData.resourceOfId,
+ parentResourceId: item.itemData?.resourceId as string,
+ resourceId: childItem.itemData.resourceId,
+ position: childIndex,
+ }
+ })
+
+ return [
+ {
+ currentParentResourceId: item.itemData.resourceOfId,
+ parentResourceId: rootResourceId,
+ resourceId: item.itemData.resourceId,
+ position: index,
+ children,
+ },
+ ]
+ })
+
+ await updateResourcePositions(resourcePositions)
+ }, [rootResourceId])
+
+ useEffect(() => {
+ if (lastAction === null) {
+ return
+ }
+
+ if (lastAction.type === 'toggle') {
+ return
+ }
+
+ saveTreeData()
+
+ if (lastAction.type === 'modal-move') {
+ const parentName =
+ lastAction.targetId === '' ? 'the root' : `Item ${lastAction.targetId}`
+
+ liveRegion.announce(
+ `You've moved Item ${lastAction.itemId} to position ${lastAction.index + 1} in ${parentName}.`,
+ )
+
+ const { element, actionMenuTrigger } =
+ registry.get(lastAction.itemId) ?? {}
+ if (element) {
+ triggerPostMoveFlash(element)
+ }
+
+ /**
+ * Only moves triggered by the modal will result in focus being
+ * returned to the trigger.
+ */
+ actionMenuTrigger?.focus()
+
+ return
+ }
+
+ if (lastAction.type === 'instruction') {
+ const { element } = registry.get(lastAction.itemId) ?? {}
+ if (element) {
+ triggerPostMoveFlash(element)
+ }
+
+ return
+ }
+ }, [lastAction, registry, saveTreeData])
+
+ useEffect(() => {
+ return () => {
+ liveRegion.cleanup()
+ }
+ }, [])
+
+ /**
+ * Returns the items that the item with `itemId` can be moved to.
+ *
+ * Uses a depth-first search (DFS) to compile a list of possible targets.
+ */
+ const getMoveTargets = useCallback(({ itemId }: { itemId: string }) => {
+ const data = lastStateRef.current
+
+ const targets = []
+
+ const searchStack = Array.from(data)
+ while (searchStack.length > 0) {
+ const node = searchStack.pop()
+
+ if (!node) {
+ continue
+ }
+
+ /**
+ * If the current node is the item we want to move, then it is not a valid
+ * move target and neither are its children.
+ */
+ if (node.id === itemId) {
+ continue
+ }
+
+ /**
+ * Draft items cannot have children.
+ */
+ if (node.isDraft) {
+ continue
+ }
+
+ targets.push(node)
+
+ node.children.forEach((childNode) => searchStack.push(childNode))
+ }
+
+ return targets
+ }, [])
+
+ const getChildrenOfItem = useCallback((itemId: string) => {
+ const data = lastStateRef.current
+
+ /**
+ * An empty string is representing the root
+ */
+ if (itemId === '') {
+ return data
+ }
+
+ const item = tree.find(data, itemId)
+ invariant(item)
+ return item.children
+ }, [])
+
+ const context = useMemo(
+ () => ({
+ dispatch: updateState,
+ uniqueContextId: Symbol('unique-id'),
+ // memoizing this function as it is called by all tree items repeatedly
+ // An ideal refactor would be to update our data shape
+ // to allow quick lookups of parents
+ getPathToItem: memoizeOne(
+ (targetId: string) =>
+ tree.getPathToItem({ current: lastStateRef.current, targetId }) ?? [],
+ ),
+ getMoveTargets,
+ getChildrenOfItem,
+ registerTreeItem,
+ rootResourceId,
+ rootResource,
+ }),
+ [
+ getChildrenOfItem,
+ getMoveTargets,
+ registerTreeItem,
+ updateState,
+ rootResourceId,
+ rootResource,
+ ],
+ )
+
+ useEffect(() => {
+ invariant(ref.current)
+ return combine(
+ monitorForElements({
+ canMonitor: ({ source }) =>
+ source.data.uniqueContextId === context.uniqueContextId,
+ onDrop(args) {
+ const { location, source } = args
+ // didn't drop on anything
+ if (!location.current.dropTargets.length) {
+ return
+ }
+
+ if (source.data.type === 'tree-item') {
+ const itemId = source.data.id as string
+
+ const target = location.current.dropTargets[0]
+
+ if (!target?.data) {
+ throw new Error('target.data is undefined')
+ }
+
+ const targetId = target.data.id as string
+
+ const instruction: Instruction | null = extractInstruction(
+ target.data,
+ )
+
+ if (instruction !== null) {
+ updateState({
+ type: 'instruction',
+ instruction,
+ itemId,
+ targetId,
+ })
+ }
+ }
+ },
+ }),
+ )
+ }, [context, extractInstruction, updateState])
+
+ return (
+
+
+
+ {data.map((item, index, array) => {
+ const type: ItemMode = (() => {
+ if (item.children.length && item.isOpen) {
+ return 'expanded'
+ }
+
+ if (index === array.length - 1) {
+ return 'last-in-group'
+ }
+
+ return 'standard'
+ })()
+
+ return
+ })}
+
+
+
+ )
+}
diff --git a/apps/egghead/src/components/resources-crud/create-post-form.tsx b/apps/egghead/src/components/resources-crud/create-post-form.tsx
new file mode 100644
index 000000000..6ed0f767a
--- /dev/null
+++ b/apps/egghead/src/components/resources-crud/create-post-form.tsx
@@ -0,0 +1,132 @@
+'use client'
+
+import * as React from 'react'
+import { NewPost, PostTypeSchema } from '@/lib/posts'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm } from 'react-hook-form'
+import { z } from 'zod'
+
+import type { ContentResource } from '@coursebuilder/core/schemas'
+import {
+ Button,
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ Input,
+} from '@coursebuilder/ui'
+
+export function CreatePostForm({
+ resourceType,
+ onCreate,
+ createPost,
+ restrictToPostType,
+ onCancel,
+}: {
+ resourceType: string
+ onCreate: (resource: ContentResource) => Promise
+ createPost: (values: NewPost) => Promise
+ restrictToPostType?: string
+ onCancel?: () => void
+}) {
+ const form = useForm<{ fields: { title: string; postType: string } }>({
+ resolver: zodResolver(
+ z.object({
+ fields: z.object({
+ title: z.string(),
+ postType: PostTypeSchema,
+ }),
+ }),
+ ),
+ defaultValues: {
+ fields: {
+ title: '',
+ postType: restrictToPostType || 'lesson',
+ },
+ },
+ })
+
+ const internalOnSubmit = async (values: {
+ fields: { title: string; postType: string }
+ }) => {
+ const resource = await createPost({
+ title: values.fields.title,
+ postType: PostTypeSchema.parse(values.fields.postType),
+ })
+ form.reset()
+ if (resource) {
+ await onCreate(resource)
+ }
+ }
+
+ return (
+
+
+ )
+}
diff --git a/apps/egghead/src/components/resources-crud/create-resource-page.tsx b/apps/egghead/src/components/resources-crud/create-resource-page.tsx
index bf495d88a..95758411a 100644
--- a/apps/egghead/src/components/resources-crud/create-resource-page.tsx
+++ b/apps/egghead/src/components/resources-crud/create-resource-page.tsx
@@ -1,10 +1,13 @@
import * as React from 'react'
import { notFound, redirect } from 'next/navigation'
+import { createPost } from '@/lib/posts-query'
import { createResource } from '@/lib/resources/create-resources'
import { getServerAuthSession } from '@/server/auth'
import type { ContentResource } from '@coursebuilder/core/schemas'
-import { CreateResourceCard } from '@coursebuilder/ui/resources-crud/create-resource-card'
+import { Card, CardContent, CardFooter, CardHeader } from '@coursebuilder/ui'
+
+import { CreatePostForm } from './create-post-form'
export const dynamic = 'force-dynamic'
@@ -21,14 +24,20 @@ export default async function CreateResourcePage({
return (
- {
- 'use server'
- redirect(`/${resource.fields?.slug}`)
- }}
- createResource={createResource}
- />
+
+
+
+ {
+ 'use server'
+ redirect(`/${resource.fields?.slug}`)
+ }}
+ createPost={createPost}
+ />
+
+
+
)
}
diff --git a/apps/egghead/src/components/resources-crud/new-resource-with-video-form.tsx b/apps/egghead/src/components/resources-crud/new-resource-with-video-form.tsx
new file mode 100644
index 000000000..3ba5a1e8f
--- /dev/null
+++ b/apps/egghead/src/components/resources-crud/new-resource-with-video-form.tsx
@@ -0,0 +1,297 @@
+'use client'
+
+import * as React from 'react'
+import {
+ NewPost,
+ POST_TYPES_WITH_VIDEO,
+ PostType,
+ PostTypeSchema,
+} from '@/lib/posts'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { FileVideo } from 'lucide-react'
+import { useForm } from 'react-hook-form'
+import { z } from 'zod'
+
+import {
+ type ContentResource,
+ type VideoResource,
+} from '@coursebuilder/core/schemas'
+import {
+ Button,
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ Input,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@coursebuilder/ui'
+import { cn } from '@coursebuilder/ui/utils/cn'
+
+const NewResourceWithVideoSchema = z.object({
+ title: z.string().min(2).max(90),
+ videoResourceId: z.string().min(4, 'Please upload a video'),
+})
+
+type NewResourceWithVideo = z.infer
+
+const FormValuesSchema = NewResourceWithVideoSchema.extend({
+ postType: PostTypeSchema,
+ videoResourceId: z.string().optional(),
+})
+
+type FormValues = z.infer
+
+export function NewResourceWithVideoForm({
+ getVideoResource,
+ createResource,
+ onResourceCreated,
+ availableResourceTypes,
+ className,
+ children,
+}: {
+ getVideoResource: (idOrSlug?: string) => Promise
+ createResource: (values: NewPost) => Promise
+ onResourceCreated: (resource: ContentResource, title: string) => Promise
+ availableResourceTypes?: PostType[] | undefined
+ className?: string
+ children: (
+ handleSetVideoResourceId: (value: string) => void,
+ ) => React.ReactNode
+}) {
+ const [videoResourceId, setVideoResourceId] = React.useState<
+ string | undefined
+ >()
+ const [videoResourceValid, setVideoResourceValid] =
+ React.useState(false)
+ const [isValidatingVideoResource, setIsValidatingVideoResource] =
+ React.useState(false)
+
+ const form = useForm({
+ resolver: zodResolver(FormValuesSchema),
+ defaultValues: {
+ title: '',
+ videoResourceId: undefined,
+ postType: availableResourceTypes?.[0] || 'lesson',
+ },
+ })
+
+ async function* pollVideoResource(
+ videoResourceId: string,
+ maxAttempts = 30,
+ initialDelay = 250,
+ delayIncrement = 250,
+ ) {
+ let delay = initialDelay
+
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ const videoResource = await getVideoResource(videoResourceId)
+ if (videoResource) {
+ yield videoResource
+ return
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, delay))
+ delay += delayIncrement
+ }
+
+ throw new Error('Video resource not found after maximum attempts')
+ }
+
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+
+ const onSubmit = async (values: FormValues) => {
+ try {
+ setIsSubmitting(true)
+ if (values.videoResourceId) {
+ await pollVideoResource(values.videoResourceId).next()
+ }
+ const resource = await createResource(values as any)
+ console.log({ resource })
+ if (!resource) {
+ // Handle edge case, e.g., toast an error message
+ console.log('no resource in onSubmit')
+ return
+ }
+
+ onResourceCreated(resource, form.watch('title'))
+ } catch (error) {
+ console.error('Error polling video resource:', error)
+ // handle error, e.g. toast an error message
+ } finally {
+ form.reset()
+ setVideoResourceId(videoResourceId)
+ form.setValue('videoResourceId', '')
+ setIsSubmitting(false)
+ }
+ }
+
+ async function handleSetVideoResourceId(videoResourceId: string) {
+ try {
+ console.log('inside handleSetVideoResourceId')
+ setVideoResourceId(videoResourceId)
+ setIsValidatingVideoResource(true)
+ form.setValue('videoResourceId', videoResourceId)
+ console.log('setValue videoResourceId: ', videoResourceId)
+ await pollVideoResource(videoResourceId).next()
+
+ setVideoResourceValid(true)
+ console.log('setting video resource valid')
+ setIsValidatingVideoResource(false)
+ } catch (error) {
+ setVideoResourceValid(false)
+ console.log('setting video resource INVALID')
+ form.setError('videoResourceId', { message: 'Video resource not found' })
+ setVideoResourceId('')
+ form.setValue('videoResourceId', '')
+ setIsValidatingVideoResource(false)
+ }
+ }
+
+ const selectedPostType = form.watch('postType')
+
+ return (
+
+
+ )
+}
diff --git a/apps/egghead/src/components/resources-crud/search-existing-lessons.tsx b/apps/egghead/src/components/resources-crud/search-existing-lessons.tsx
new file mode 100644
index 000000000..7e80a9bcf
--- /dev/null
+++ b/apps/egghead/src/components/resources-crud/search-existing-lessons.tsx
@@ -0,0 +1,111 @@
+'use client'
+
+import * as React from 'react'
+import { useDebounce } from '@/lib/hooks/use-debounce'
+import { searchLessons } from '@/lib/posts-query'
+import { ChevronsUpDown } from 'lucide-react'
+
+import type { ContentResource } from '@coursebuilder/core/schemas'
+import {
+ Button,
+ Command,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@coursebuilder/ui'
+
+type SearchExistingLessonsProps = {
+ onSelect: (resource: ContentResource) => void
+ onCancel: () => void
+}
+
+export function SearchExistingLessons({
+ onSelect,
+ onCancel,
+}: SearchExistingLessonsProps) {
+ const [open, setOpen] = React.useState(false)
+ const [searchTerm, setSearchTerm] = React.useState('')
+ const [results, setResults] = React.useState([])
+ const [isSearching, setIsSearching] = React.useState(false)
+ const debouncedSearchTerm = useDebounce(searchTerm, 300)
+
+ React.useEffect(() => {
+ async function search() {
+ if (!debouncedSearchTerm) {
+ setResults([])
+ return
+ }
+
+ setIsSearching(true)
+ try {
+ const searchResults = await searchLessons(debouncedSearchTerm)
+ setResults(searchResults)
+ } catch (error) {
+ console.error('Search error:', error)
+ setResults([])
+ } finally {
+ setIsSearching(false)
+ }
+ }
+
+ search()
+ }, [debouncedSearchTerm])
+
+ return (
+
+
+
+
+
+ Search lessons...
+
+
+
+
+
+
+ {isSearching ? (
+
+ Searching...
+
+ ) : results.length > 0 ? (
+
+ {results.map((result) => (
+ {
+ onSelect(result)
+ setOpen(false)
+ }}
+ >
+ {result.fields?.title || 'Untitled'}
+
+ ))}
+
+ ) : searchTerm.length >= 2 ? (
+
+ No lessons found
+
+ ) : null}
+
+
+
+
+ Cancel
+
+
+
+ )
+}
diff --git a/apps/egghead/src/inngest/functions/migrate-tips-to-posts.ts b/apps/egghead/src/inngest/functions/migrate-tips-to-posts.ts
index 9137eed7d..7d9867589 100644
--- a/apps/egghead/src/inngest/functions/migrate-tips-to-posts.ts
+++ b/apps/egghead/src/inngest/functions/migrate-tips-to-posts.ts
@@ -14,7 +14,7 @@ import { Post, PostSchema } from '@/lib/posts'
import { createNewPostVersion, getPost } from '@/lib/posts-query'
import { EggheadTagSchema } from '@/lib/tags'
import { guid } from '@/utils/guid'
-import { createClient } from '@sanity/client'
+// import { createClient } from '@sanity/client'
import slugify from '@sindresorhus/slugify'
import { eq, or, sql } from 'drizzle-orm'
import { z } from 'zod'
@@ -59,29 +59,29 @@ export const TipsUpdatedEventSchema = z.object({
export type TipsUpdatedEvent = z.infer
-export const sanityWriteClient = createClient({
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET_ID || 'production',
- useCdn: false, // `false` if you want to ensure fresh data
- apiVersion: process.env.NEXT_PUBLIC_SANITY_API_VERSION,
- token: process.env.SANITY_EDITOR_TOKEN,
-})
+// export const sanityWriteClient = createClient({
+// projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
+// dataset: process.env.NEXT_PUBLIC_SANITY_DATASET_ID || 'production',
+// useCdn: false, // `false` if you want to ensure fresh data
+// apiVersion: process.env.NEXT_PUBLIC_SANITY_API_VERSION,
+// token: process.env.SANITY_EDITOR_TOKEN,
+// })
export const migrateTipsToPosts = inngest.createFunction(
{ id: 'migrate-tips-to-posts', name: 'Migrate Tips to Posts' },
{ event: TIPS_UPDATED_EVENT },
async ({ event, step }) => {
const tips = await step.run('Load tips from sanity', async () => {
- const tips = await sanityWriteClient.fetch(`*[_type == "tip"]{
- ...,
- "resources": resources[]->,
- "collaborators": collaborators[]->,
- "softwareLibraries": softwareLibraries[]{
- ...,
- "library": library->
- }
- }`)
- return tips
+ // const tips = await sanityWriteClient.fetch(`*[_type == "tip"]{
+ // ...,
+ // "resources": resources[]->,
+ // "collaborators": collaborators[]->,
+ // "softwareLibraries": softwareLibraries[]{
+ // ...,
+ // "library": library->
+ // }
+ // }`)
+ return [] as any
})
for (const loadedTip of tips) {
diff --git a/apps/egghead/src/lib/hooks/use-debounce.ts b/apps/egghead/src/lib/hooks/use-debounce.ts
new file mode 100644
index 000000000..01419802c
--- /dev/null
+++ b/apps/egghead/src/lib/hooks/use-debounce.ts
@@ -0,0 +1,17 @@
+import * as React from 'react'
+
+export function useDebounce(value: T, delay: number): T {
+ const [debouncedValue, setDebouncedValue] = React.useState(value)
+
+ React.useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value)
+ }, delay)
+
+ return () => {
+ clearTimeout(handler)
+ }
+ }, [value, delay])
+
+ return debouncedValue
+}
diff --git a/apps/egghead/src/lib/posts-query.ts b/apps/egghead/src/lib/posts-query.ts
index 0ed7dcf76..f59cde7a1 100644
--- a/apps/egghead/src/lib/posts-query.ts
+++ b/apps/egghead/src/lib/posts-query.ts
@@ -26,7 +26,7 @@ import { getServerAuthSession } from '@/server/auth'
import { guid } from '@/utils/guid'
import { subject } from '@casl/ability'
import slugify from '@sindresorhus/slugify'
-import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
+import { and, asc, desc, eq, like, or, sql } from 'drizzle-orm'
import readingTime from 'reading-time'
import Typesense from 'typesense'
import { z } from 'zod'
@@ -35,8 +35,13 @@ import 'server-only'
import { POST_CREATED_EVENT } from '@/inngest/events/post-created'
import { inngest } from '@/inngest/inngest.server'
+import { last } from 'lodash'
import { getMuxAsset } from '@coursebuilder/core/lib/mux'
+import {
+ ContentResource,
+ ContentResourceSchema,
+} from '@coursebuilder/core/schemas'
import {
createEggheadLesson,
@@ -49,18 +54,45 @@ import {
import { EggheadTag, EggheadTagSchema } from './tags'
import { upsertPostToTypeSense } from './typesense'
+export async function searchLessons(searchTerm: string) {
+ const { session } = await getServerAuthSession()
+ const userId = session?.user?.id
+
+ const lessons = await db.query.contentResource.findMany({
+ where: and(
+ eq(contentResource.type, 'post'),
+ sql`JSON_EXTRACT(${contentResource.fields}, '$.postType') = 'lesson'`,
+ or(
+ sql`LOWER(JSON_EXTRACT(${contentResource.fields}, '$.title')) LIKE ${`%${searchTerm.toLowerCase()}%`}`,
+ sql`LOWER(JSON_EXTRACT(${contentResource.fields}, '$.body')) LIKE ${`%${searchTerm.toLowerCase()}%`}`,
+ ),
+ ),
+ orderBy: [
+ // Sort by createdById matching current user (if logged in)
+ sql`CASE
+ WHEN ${contentResource.createdById} = ${userId} THEN 0
+ ELSE 1
+ END`,
+ // Secondary sort by title match (prioritize title matches)
+ sql`CASE
+ WHEN LOWER(JSON_EXTRACT(${contentResource.fields}, '$.title')) LIKE ${`%${searchTerm.toLowerCase()}%`} THEN 0
+ ELSE 1
+ END`,
+ // Then sort by title alphabetically
+ sql`JSON_EXTRACT(${contentResource.fields}, '$.title')`,
+ ],
+ })
+
+ return ContentResourceSchema.array().parse(lessons)
+}
+
export async function deletePost(id: string) {
+ console.log('deleting post', id)
const { session, ability } = await getServerAuthSession()
const user = session?.user
+ const postData = await getPost(id)
- const post = PostSchema.nullish().parse(
- await db.query.contentResource.findFirst({
- where: eq(contentResource.id, id),
- with: {
- resources: true,
- },
- }),
- )
+ const post = PostSchema.nullish().parse(postData)
if (!post) {
throw new Error(`Post with id ${id} not found.`)
@@ -200,6 +232,8 @@ export async function getAllPostsForUser(userId?: string): Promise {
}
export async function createPost(input: NewPost) {
+ console.log('createPost', input)
+
const { session, ability } = await getServerAuthSession()
if (!session?.user?.id || !ability.can('create', 'Content')) {
@@ -315,13 +349,15 @@ export async function writeNewPostToDatabase(input: {
const newPostId = `post_${postGuid}`
const videoResource =
await courseBuilderAdapter.getVideoResource(videoResourceId)
-
- const eggheadLessonId = await createEggheadLesson({
- title: title,
- slug: `${slugify(title)}~${postGuid}`,
- instructorId: eggheadInstructorId,
- guid: postGuid,
- })
+ const TYPES_WITH_LESSONS = ['lesson', 'podcast', 'tip']
+ const eggheadLessonId = TYPES_WITH_LESSONS.includes(input.newPost.postType)
+ ? await createEggheadLesson({
+ title: title,
+ slug: `${slugify(title)}~${postGuid}`,
+ instructorId: eggheadInstructorId,
+ guid: postGuid,
+ })
+ : null
await db
.insert(contentResource)
@@ -333,8 +369,9 @@ export async function writeNewPostToDatabase(input: {
title,
state: 'draft',
visibility: 'unlisted',
+ postType: input.newPost.postType,
slug: `${slugify(title)}~${postGuid}`,
- eggheadLessonId,
+ ...(eggheadLessonId ? { eggheadLessonId } : {}),
},
})
.catch((error) => {
@@ -450,14 +487,7 @@ export async function writePostUpdateToDatabase(input: {
}
export async function deletePostFromDatabase(id: string) {
- const post = PostSchema.nullish().parse(
- await db.query.contentResource.findFirst({
- where: eq(contentResource.id, id),
- with: {
- resources: true,
- },
- }),
- )
+ const post = PostSchema.nullish().parse(await getPost(id))
if (!post) {
throw new Error(`Post with id ${id} not found.`)
@@ -544,3 +574,128 @@ export async function getVideoDuration(
}
return 0
}
+
+export const addResourceToResource = async ({
+ resource,
+ resourceId,
+}: {
+ resource: ContentResource
+ resourceId: string
+}) => {
+ const parentResource = await db.query.contentResource.findFirst({
+ where: like(contentResource.id, `%${last(resourceId.split('-'))}%`),
+ with: {
+ resources: true,
+ },
+ })
+
+ if (!parentResource) {
+ throw new Error(`Workshop with id ${resourceId} not found`)
+ }
+ await db.insert(contentResourceResource).values({
+ resourceOfId: parentResource.id,
+ resourceId: resource.id,
+ position: parentResource.resources.length,
+ })
+
+ const resourceResource = db.query.contentResourceResource.findFirst({
+ where: and(
+ eq(contentResourceResource.resourceOfId, parentResource.id),
+ eq(contentResourceResource.resourceId, resource.id),
+ ),
+ with: {
+ resource: true,
+ },
+ })
+
+ revalidateTag('posts')
+
+ return resourceResource
+}
+
+export const updateResourcePosition = async ({
+ currentParentResourceId,
+ parentResourceId,
+ resourceId,
+ position,
+}: {
+ currentParentResourceId: string
+ parentResourceId: string
+ resourceId: string
+ position: number
+}) => {
+ const result = await db
+ .update(contentResourceResource)
+ .set({ position, resourceOfId: parentResourceId })
+ .where(
+ and(
+ eq(contentResourceResource.resourceOfId, currentParentResourceId),
+ eq(contentResourceResource.resourceId, resourceId),
+ ),
+ )
+
+ revalidateTag('posts')
+
+ return result
+}
+
+type positionInputIten = {
+ currentParentResourceId: string
+ parentResourceId: string
+ resourceId: string
+ position: number
+ children?: positionInputIten[]
+}
+
+export const updateResourcePositions = async (input: positionInputIten[]) => {
+ const result = await db.transaction(async (trx) => {
+ for (const {
+ currentParentResourceId,
+ parentResourceId,
+ resourceId,
+ position,
+ children,
+ } of input) {
+ await trx
+ .update(contentResourceResource)
+ .set({ position, resourceOfId: parentResourceId })
+ .where(
+ and(
+ eq(contentResourceResource.resourceOfId, currentParentResourceId),
+ eq(contentResourceResource.resourceId, resourceId),
+ ),
+ )
+ for (const child of children || []) {
+ await trx
+ .update(contentResourceResource)
+ .set({
+ position: child.position,
+ resourceOfId: child.parentResourceId,
+ })
+ .where(
+ and(
+ eq(
+ contentResourceResource.resourceOfId,
+ child.currentParentResourceId,
+ ),
+ eq(contentResourceResource.resourceId, child.resourceId),
+ ),
+ )
+ }
+ }
+ })
+
+ return result
+}
+
+export async function removeSection(
+ sectionId: string,
+ pathToRevalidate: string,
+) {
+ await db.delete(contentResource).where(eq(contentResource.id, sectionId))
+
+ revalidatePath(pathToRevalidate)
+ return await db
+ .delete(contentResourceResource)
+ .where(eq(contentResourceResource.resourceId, sectionId))
+}
diff --git a/apps/egghead/src/lib/posts.ts b/apps/egghead/src/lib/posts.ts
index fe0086104..547b2aa0d 100644
--- a/apps/egghead/src/lib/posts.ts
+++ b/apps/egghead/src/lib/posts.ts
@@ -5,6 +5,8 @@ import { z } from 'zod'
import { ContentResourceSchema } from '@coursebuilder/core/schemas/content-resource-schema'
+export const POST_TYPES_WITH_VIDEO = ['lesson', 'podcast', 'tip']
+
export const PostActionSchema = z.union([
z.literal('publish'),
z.literal('unpublish'),
@@ -65,6 +67,7 @@ export type Post = z.infer
export const NewPostSchema = z.object({
title: z.string().min(2).max(90),
+ postType: PostTypeSchema.default('lesson'),
videoResourceId: z.string().min(4, 'Please upload a video').nullish(),
})
diff --git a/apps/egghead/src/lib/tags-query.ts b/apps/egghead/src/lib/tags-query.ts
index 86e86bc77..f99a75ef3 100644
--- a/apps/egghead/src/lib/tags-query.ts
+++ b/apps/egghead/src/lib/tags-query.ts
@@ -1,3 +1,5 @@
+'use server'
+
import { revalidateTag, unstable_cache } from 'next/cache'
import { db } from '@/db'
import { eggheadPgQuery } from '@/db/eggheadPostgres'
diff --git a/apps/egghead/src/styles/globals.css b/apps/egghead/src/styles/globals.css
index 51102920a..5b6ce5702 100644
--- a/apps/egghead/src/styles/globals.css
+++ b/apps/egghead/src/styles/globals.css
@@ -1,9 +1,9 @@
+@import './login.css';
+
@tailwind base;
@tailwind components;
@tailwind utilities;
-@import './login.css';
-
@layer base {
:root {
--nav-height: 3.5rem;
diff --git a/apps/egghead/tsconfig.json b/apps/egghead/tsconfig.json
index c77745a6e..beeba99ed 100644
--- a/apps/egghead/tsconfig.json
+++ b/apps/egghead/tsconfig.json
@@ -34,7 +34,8 @@
"**/*.cjs",
"**/*.mjs",
".next/types/**/*.ts",
- "./node_modules/y-codemirror.next/dist/src/index.d.ts"
+ "./node_modules/y-codemirror.next/dist/src/index.d.ts",
+ "tailwind.config.ts"
],
"exclude": ["node_modules"]
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 84c76d583..ec47b3d0e 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -40,7 +40,7 @@
"@radix-ui/react-tooltip": "^1.1.3",
"@types/md5": "^2.3.5",
"@uiw/codemirror-themes": "^4.21.24",
- "@uiw/react-codemirror": "^4.23.0",
+ "@uiw/react-codemirror": "^4.23.6",
"@uiw/react-markdown-editor": "^6.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index aab24f4de..075b4019e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2178,6 +2178,21 @@ importers:
apps/egghead:
dependencies:
+ '@atlaskit/pragmatic-drag-and-drop':
+ specifier: ^1.1.3
+ version: 1.1.3
+ '@atlaskit/pragmatic-drag-and-drop-flourish':
+ specifier: ^1.0.4
+ version: 1.0.4(react@19.0.0-rc-02c0e824-20241028)(types-react@19.0.0-rc.1)
+ '@atlaskit/pragmatic-drag-and-drop-hitbox':
+ specifier: ^1.0.3
+ version: 1.0.3
+ '@atlaskit/pragmatic-drag-and-drop-live-region':
+ specifier: ^1.0.3
+ version: 1.0.3
+ '@atlaskit/tokens':
+ specifier: ^1.42.1
+ version: 1.42.1(react@19.0.0-rc-02c0e824-20241028)
'@auth/core':
specifier: ^0.37.2
version: 0.37.2(nodemailer@6.9.11)
@@ -2331,6 +2346,9 @@ importers:
lucide-react:
specifier: ^0.288.0
version: 0.288.0(react@19.0.0-rc-02c0e824-20241028)
+ memoize-one:
+ specifier: ^6.0.0
+ version: 6.0.0
nanoid:
specifier: ^5.0.2
version: 5.0.6
@@ -2412,6 +2430,12 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.1)
+ tailwindcss-radix:
+ specifier: ^2.8.0
+ version: 2.9.0
+ tiny-invariant:
+ specifier: ^1.3.1
+ version: 1.3.3
typesense:
specifier: ^1.8.2
version: 1.8.2(@babel/runtime@7.26.0)
@@ -2446,6 +2470,9 @@ importers:
'@types/react':
specifier: npm:types-react@19.0.0-rc.1
version: /types-react@19.0.0-rc.1
+ '@types/react-dom':
+ specifier: npm:types-react-dom@19.0.0-rc.1
+ version: /types-react-dom@19.0.0-rc.1
'@types/react-gravatar':
specifier: ^2.6.13
version: 2.6.14
@@ -6482,8 +6509,8 @@ importers:
specifier: ^4.21.24
version: 4.21.24(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0)
'@uiw/react-codemirror':
- specifier: ^4.23.0
- version: 4.23.0(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.0)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)
+ specifier: ^4.23.6
+ version: 4.23.6(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.0)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)
'@uiw/react-markdown-editor':
specifier: ^6.1.2
version: 6.1.2(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.0)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)(types-react@19.0.0-rc.1)
@@ -6997,7 +7024,7 @@ packages:
peerDependencies:
react: ^16.8.0
dependencies:
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
bind-event-listener: 2.1.1
react: 19.0.0-rc-02c0e824-20241028
dev: false
@@ -7110,7 +7137,7 @@ packages:
react: ^16.8.0
dependencies:
'@atlaskit/ds-lib': 2.2.4(react@19.0.0-rc-02c0e824-20241028)
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
'@emotion/react': 11.11.4(react@19.0.0-rc-02c0e824-20241028)(types-react@19.0.0-rc.1)
bind-event-listener: 2.1.1
react: 19.0.0-rc-02c0e824-20241028
@@ -7122,7 +7149,7 @@ packages:
/@atlaskit/platform-feature-flags@0.2.5:
resolution: {integrity: sha512-0fD2aDxn2mE59D4acUhVib+YF2HDYuuPH50aYwpQdcV/CsVkAaJsMKy8WhWSulcRFeMYp72kfIfdy0qGdRB7Uw==}
dependencies:
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
dev: false
/@atlaskit/portal@4.4.0(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028):
@@ -8239,7 +8266,7 @@ packages:
resolution: {integrity: sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.24.9
+ '@babel/types': 7.26.0
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
jsesc: 2.5.2
@@ -8300,7 +8327,7 @@ packages:
resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.24.9
+ '@babel/types': 7.26.0
dev: false
/@babel/helper-function-name@7.23.0:
@@ -8308,27 +8335,27 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.24.0
- '@babel/types': 7.24.0
+ '@babel/types': 7.26.0
/@babel/helper-function-name@7.24.7:
resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.24.7
- '@babel/types': 7.24.9
+ '@babel/types': 7.26.0
dev: false
/@babel/helper-hoist-variables@7.22.5:
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.24.0
+ '@babel/types': 7.26.0
/@babel/helper-hoist-variables@7.24.7:
resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.24.9
+ '@babel/types': 7.26.0
dev: false
/@babel/helper-module-imports@7.22.15:
@@ -8406,7 +8433,7 @@ packages:
resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.24.0
+ '@babel/types': 7.26.0
/@babel/helper-simple-access@7.24.7:
resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==}
@@ -8422,13 +8449,13 @@ packages:
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.24.0
+ '@babel/types': 7.26.0
/@babel/helper-split-export-declaration@7.24.7:
resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==}
engines: {node: '>=6.9.0'}
dependencies:
- '@babel/types': 7.24.9
+ '@babel/types': 7.26.0
dev: false
/@babel/helper-string-parser@7.23.4:
@@ -8475,8 +8502,8 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.24.0
- '@babel/traverse': 7.24.0
- '@babel/types': 7.24.0
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
transitivePeerDependencies:
- supports-color
@@ -8533,7 +8560,7 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
- '@babel/types': 7.24.9
+ '@babel/types': 7.26.0
dev: false
/@babel/parser@7.26.2:
@@ -8744,7 +8771,7 @@ packages:
dependencies:
'@babel/code-frame': 7.23.5
'@babel/parser': 7.24.0
- '@babel/types': 7.24.0
+ '@babel/types': 7.26.0
/@babel/template@7.24.7:
resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
@@ -8752,7 +8779,7 @@ packages:
dependencies:
'@babel/code-frame': 7.26.2
'@babel/parser': 7.24.8
- '@babel/types': 7.24.9
+ '@babel/types': 7.26.0
dev: false
/@babel/template@7.25.9:
@@ -13299,7 +13326,7 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@19.0.0-rc-02c0e824-20241028)
'@radix-ui/react-context': 1.0.0(react@19.0.0-rc-02c0e824-20241028)
@@ -20367,7 +20394,7 @@ packages:
'@ucast/core': 1.10.2
dev: false
- /@uiw/codemirror-extensions-basic-setup@4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0):
+ /@uiw/codemirror-extensions-basic-setup@4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.28.5):
resolution: {integrity: sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
@@ -20378,17 +20405,17 @@ packages:
'@codemirror/state': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
dependencies:
- '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0)(@lezer/common@1.2.3)
+ '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.28.5)(@lezer/common@1.2.3)
'@codemirror/commands': 6.6.0
'@codemirror/language': 6.10.3
'@codemirror/lint': 6.8.2
'@codemirror/search': 6.5.6
'@codemirror/state': 6.4.1
- '@codemirror/view': 6.26.0
+ '@codemirror/view': 6.28.5
dev: false
- /@uiw/codemirror-extensions-basic-setup@4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.28.5):
- resolution: {integrity: sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==}
+ /@uiw/codemirror-extensions-basic-setup@4.23.6(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0):
+ resolution: {integrity: sha512-bvtq8IOvdkLJMhoJBRGPEzU51fMpPDwEhcAHp9xCR05MtbIokQgsnLXrmD1aZm6e7s/3q47H+qdSfAAkR5MkLA==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
'@codemirror/commands': '>=6.0.0'
@@ -20398,17 +20425,17 @@ packages:
'@codemirror/state': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
dependencies:
- '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.28.5)(@lezer/common@1.2.3)
+ '@codemirror/autocomplete': 6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0)(@lezer/common@1.2.3)
'@codemirror/commands': 6.6.0
'@codemirror/language': 6.10.3
'@codemirror/lint': 6.8.2
'@codemirror/search': 6.5.6
'@codemirror/state': 6.4.1
- '@codemirror/view': 6.28.5
+ '@codemirror/view': 6.26.0
dev: false
- /@uiw/codemirror-extensions-basic-setup@4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1):
- resolution: {integrity: sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==}
+ /@uiw/codemirror-extensions-basic-setup@4.23.6(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1):
+ resolution: {integrity: sha512-bvtq8IOvdkLJMhoJBRGPEzU51fMpPDwEhcAHp9xCR05MtbIokQgsnLXrmD1aZm6e7s/3q47H+qdSfAAkR5MkLA==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
'@codemirror/commands': '>=6.0.0'
@@ -20491,7 +20518,7 @@ packages:
resolution: {integrity: sha512-O2GUHV90Iw2VrSLVLK0OmNIMdZ5fgEg4NhvtwINsX+eZ/Wf6DWD0TdsK9xwV7dNRnK/UI2mQtl0a2/kRgm1m1A==}
dev: false
- /@uiw/react-codemirror@4.23.0(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.0)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028):
+ /@uiw/react-codemirror@4.23.0(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.28.5)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028):
resolution: {integrity: sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==}
peerDependencies:
'@babel/runtime': '>=7.11.0'
@@ -20506,8 +20533,8 @@ packages:
'@codemirror/commands': 6.6.0
'@codemirror/state': 6.4.1
'@codemirror/theme-one-dark': 6.1.2
- '@codemirror/view': 6.26.0
- '@uiw/codemirror-extensions-basic-setup': 4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0)
+ '@codemirror/view': 6.28.5
+ '@uiw/codemirror-extensions-basic-setup': 4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.28.5)
codemirror: 6.0.1(@lezer/common@1.2.3)
react: 19.0.0-rc-02c0e824-20241028
react-dom: 19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028)
@@ -20518,8 +20545,8 @@ packages:
- '@codemirror/search'
dev: false
- /@uiw/react-codemirror@4.23.0(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.28.5)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028):
- resolution: {integrity: sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==}
+ /@uiw/react-codemirror@4.23.6(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.0)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028):
+ resolution: {integrity: sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==}
peerDependencies:
'@babel/runtime': '>=7.11.0'
'@codemirror/state': '>=6.0.0'
@@ -20533,8 +20560,8 @@ packages:
'@codemirror/commands': 6.6.0
'@codemirror/state': 6.4.1
'@codemirror/theme-one-dark': 6.1.2
- '@codemirror/view': 6.28.5
- '@uiw/codemirror-extensions-basic-setup': 4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.28.5)
+ '@codemirror/view': 6.26.0
+ '@uiw/codemirror-extensions-basic-setup': 4.23.6(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0)
codemirror: 6.0.1(@lezer/common@1.2.3)
react: 19.0.0-rc-02c0e824-20241028
react-dom: 19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028)
@@ -20545,8 +20572,8 @@ packages:
- '@codemirror/search'
dev: false
- /@uiw/react-codemirror@4.23.0(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.34.1)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028):
- resolution: {integrity: sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==}
+ /@uiw/react-codemirror@4.23.6(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.34.1)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028):
+ resolution: {integrity: sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==}
peerDependencies:
'@babel/runtime': '>=7.11.0'
'@codemirror/state': '>=6.0.0'
@@ -20561,7 +20588,7 @@ packages:
'@codemirror/state': 6.4.1
'@codemirror/theme-one-dark': 6.1.2
'@codemirror/view': 6.34.1
- '@uiw/codemirror-extensions-basic-setup': 4.23.0(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)
+ '@uiw/codemirror-extensions-basic-setup': 4.23.6(@codemirror/autocomplete@6.18.2)(@codemirror/commands@6.6.0)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)
codemirror: 6.0.1(@lezer/common@1.2.3)
react: 19.0.0-rc-02c0e824-20241028
react-dom: 19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028)
@@ -20584,7 +20611,7 @@ packages:
'@codemirror/language-data': 6.4.1(@codemirror/view@6.26.0)
'@uiw/codemirror-extensions-events': 4.23.0(@codemirror/view@6.26.0)
'@uiw/codemirror-themes': 4.21.24(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.26.0)
- '@uiw/react-codemirror': 4.23.0(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.0)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)
+ '@uiw/react-codemirror': 4.23.6(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.0)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)
'@uiw/react-markdown-preview': 5.1.2(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)(types-react@19.0.0-rc.1)
react: 19.0.0-rc-02c0e824-20241028
react-dom: 19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028)
@@ -20642,7 +20669,7 @@ packages:
'@codemirror/language-data': 6.4.1(@codemirror/view@6.34.1)
'@uiw/codemirror-extensions-events': 4.23.0(@codemirror/view@6.34.1)
'@uiw/codemirror-themes': 4.21.24(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)
- '@uiw/react-codemirror': 4.23.0(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.34.1)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)
+ '@uiw/react-codemirror': 4.23.6(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.2)(@codemirror/language@6.10.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.34.1)(codemirror@6.0.1)(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)
'@uiw/react-markdown-preview': 5.1.2(react-dom@19.0.0-rc-02c0e824-20241028)(react@19.0.0-rc-02c0e824-20241028)(types-react@19.0.0-rc.1)
react: 19.0.0-rc-02c0e824-20241028
react-dom: 19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028)
@@ -20665,7 +20692,7 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
'@uiw/copy-to-clipboard': 1.0.17
react: 19.0.0-rc-02c0e824-20241028
react-dom: 19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028)
@@ -30342,7 +30369,7 @@ packages:
resolution: {integrity: sha512-+V2TDw3tXUVEptFvLSerz125C2ogYl8klIBRY1m5BHd4JvGVf3yhx8N3PngByCzA6PGcv/eydGQN+wy34SHf0Q==}
hasBin: true
dependencies:
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
chokidar: 3.6.0
glob: 10.3.10
html-minifier: 4.0.0
@@ -30541,7 +30568,7 @@ packages:
/mjml-preset-core@4.15.3(encoding@0.1.13):
resolution: {integrity: sha512-1zZS8P4O0KweWUqNS655+oNnVMPQ1Rq1GaZq5S9JfwT1Vh/m516lSmiTW9oko6gGHytt5s6Yj6oOeu5Zm8FoLw==}
dependencies:
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
mjml-accordion: 4.15.3(encoding@0.1.13)
mjml-body: 4.15.3(encoding@0.1.13)
mjml-button: 4.15.3(encoding@0.1.13)
@@ -30634,7 +30661,7 @@ packages:
/mjml-validator@4.15.3:
resolution: {integrity: sha512-Xb72KdqRwjv/qM2rJpV22syyP2N3cRQ9VVDrN6u2FSzLq02buFNxmSPJ7CKhat3PrUNdVHU75KZwOf/tz4UEhA==}
dependencies:
- '@babel/runtime': 7.24.0
+ '@babel/runtime': 7.26.0
dev: false
/mjml-wrapper@4.15.3(encoding@0.1.13):