Skip to content

Commit

Permalink
feat(aih): edit and create lists
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtaholik committed Jan 8, 2025
1 parent 28c809f commit 028bd3c
Show file tree
Hide file tree
Showing 27 changed files with 2,387 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as React from 'react'
import { useRouter } from 'next/navigation'
import type { List, ListSchema } from '@/lib/lists'
import type { UseFormReturn } from 'react-hook-form'
import { z } from 'zod'

import { VideoResource } from '@coursebuilder/core/schemas/video-resource'
import {
Button,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Textarea,
} from '@coursebuilder/ui'
import { useSocket } from '@coursebuilder/ui/hooks/use-socket'
import { MetadataFieldState } from '@coursebuilder/ui/resources-crud/metadata-fields/metadata-field-state'
import { MetadataFieldVisibility } from '@coursebuilder/ui/resources-crud/metadata-fields/metadata-field-visibility'
import AdvancedTagSelector from '@coursebuilder/ui/resources-crud/tag-selector'

export const ListMetadataFormFields: React.FC<{
form: UseFormReturn<z.infer<typeof ListSchema>>
list: List
}> = ({ form, list }) => {
const router = useRouter()

return (
<>
<FormField
control={form.control}
name="id"
render={({ field }) => <Input type="hidden" {...field} />}
/>

<FormField
control={form.control}
name="fields.title"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">Title</FormLabel>
<FormDescription>
A title should summarize the post and explain what it is about
clearly.
</FormDescription>
<Input {...field} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fields.slug"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">Slug</FormLabel>
<FormDescription>Short with keywords is best.</FormDescription>
<Input {...field} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fields.description"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">
SEO Description ({`${field.value?.length ?? '0'} / 160`})
</FormLabel>
<FormDescription>
A short snippet that summarizes the post in under 160 characters.
Keywords should be included to support SEO.
</FormDescription>
<Textarea {...field} value={field.value ?? ''} />
{field.value && field.value.length > 160 && (
<FormMessage>
Your description is longer than 160 characters
</FormMessage>
)}
<FormMessage />
</FormItem>
)}
/>
<MetadataFieldVisibility form={form} />
<MetadataFieldState form={form} />
<FormField
control={form.control}
name="fields.github"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">GitHub</FormLabel>
<FormDescription>
Direct link to the GitHub file associated with the post.
</FormDescription>
<Input {...field} value={field.value ?? ''} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fields.gitpod"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">Gitpod</FormLabel>
<FormDescription>
Gitpod link to start a new workspace with the post.
</FormDescription>
<Input {...field} value={field.value ?? ''} />
<FormMessage />
</FormItem>
)}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client'

import * as React from 'react'
import { PostMetadataFormFields } from '@/app/(content)/posts/_components/edit-post-form-metadata'
import { ImageResourceUploader } from '@/components/image-uploader/image-resource-uploader'
import { env } from '@/env.mjs'
import { useIsMobile } from '@/hooks/use-is-mobile'
import { sendResourceChatMessage } from '@/lib/ai-chat-query'
import { List, ListSchema } from '@/lib/lists'
import { Post, PostSchema } from '@/lib/posts'
import { autoUpdatePost, updatePost } from '@/lib/posts-query'
import type { Tag } from '@/lib/tags'
import { zodResolver } from '@hookform/resolvers/zod'
import { ImagePlusIcon } from 'lucide-react'
import { useSession } from 'next-auth/react'
import { useTheme } from 'next-themes'
import { useForm, type UseFormReturn } from 'react-hook-form'
import { z } from 'zod'

import { ContentResourceSchema } from '@coursebuilder/core/schemas'
import { VideoResource } from '@coursebuilder/core/schemas/video-resource'
import { EditResourcesFormDesktop } from '@coursebuilder/ui/resources-crud/edit-resources-form-desktop'

import { ListMetadataFormFields } from './edit-list-form-metadata'
import ListResoucesEdit from './list-resources-edit'

// import { MobileEditPostForm } from './edit-post-form-mobile'

const NewPostFormSchema = z.object({
title: z.string().min(2).max(90),
body: z.string().nullish(),
visibility: z.enum(['public', 'unlisted', 'private']),
description: z.string().nullish(),
github: z.string().nullish(),
gitpod: z.string().nullish(),
})

export type EditListFormProps = {
list: List
form: UseFormReturn<z.infer<typeof ListSchema>>
children?: React.ReactNode
availableWorkflows?: { value: string; label: string; default?: boolean }[]
theme?: string
}

export function EditListForm({ list }: Omit<EditListFormProps, 'form'>) {
const { forcedTheme: theme } = useTheme()
const session = useSession()
const form = useForm<z.infer<typeof ListSchema>>({
resolver: zodResolver(NewPostFormSchema),
defaultValues: {
id: list.id,
fields: {
title: list.fields?.title,
body: list.fields?.body,
slug: list.fields?.slug,
visibility: list.fields?.visibility || 'public',
state: list.fields?.state || 'draft',
description: list.fields?.description ?? '',
github: list.fields?.github ?? '',
gitpod: list.fields?.gitpod ?? '',
},
},
})
const isMobile = useIsMobile()

return (
<EditResourcesFormDesktop
resource={list}
resourceSchema={ListSchema}
getResourcePath={(slug) => `/${slug}`}
updateResource={updatePost}
autoUpdateResource={autoUpdatePost}
form={form}
bodyPanelSlot={<ListResoucesEdit list={list} />}
availableWorkflows={[]}
sendResourceChatMessage={sendResourceChatMessage}
hostUrl={env.NEXT_PUBLIC_PARTY_KIT_URL}
user={session?.data?.user}
theme={theme}
onResourceBodyChange={() => {}}
tools={[
{ id: 'assistant' },
{
id: 'media',
icon: () => (
<ImagePlusIcon strokeWidth={1.5} size={24} width={18} height={18} />
),
toolComponent: (
<ImageResourceUploader
key={'image-uploader'}
belongsToResourceId={list.id}
uploadDirectory={`posts`}
/>
),
},
]}
>
<React.Suspense fallback={<div>loading</div>}>
<ListMetadataFormFields form={form} list={list} />
</React.Suspense>
</EditResourcesFormDesktop>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client'

import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { addPostToList } from '@/lib/lists-query'
import type { TypesenseResource } from '@/lib/typesense'

import { Button } from '@coursebuilder/ui'

import type { TreeAction } from './lesson-list/data/tree'
import { useSelection } from './selection-context'

export default function Hit({
hit,
listId,
updateTreeState,
}: {
hit: TypesenseResource
listId: string
updateTreeState: React.ActionDispatch<[action: TreeAction]>
}) {
const { toggleSelection, isSelected } = useSelection()

return (
<li className={`${isSelected(hit.id) ? 'bg-muted' : ''}`}>
<button
type="button"
className="hover:bg-muted/50 group flex w-full flex-row items-baseline justify-between gap-2 px-5 py-3 sm:py-3"
onClick={() => toggleSelection(hit)}
>
<div className="flex items-center gap-2">
<span className="pr-5 font-medium sm:truncate">{hit.title}</span>
</div>
<div className="text-muted-foreground fon-normal flex flex-shrink-0 items-center gap-3 pl-7 text-xs capitalize opacity-60 sm:pl-0">
<span>{hit.type}</span>
</div>
</button>
</li>
)
}
Loading

0 comments on commit 028bd3c

Please sign in to comment.