Skip to content

Commit

Permalink
Merge branch 'main' into jb/redirect-from-unauthorized-page
Browse files Browse the repository at this point in the history
  • Loading branch information
joelhooks authored Jan 4, 2024
2 parents 60e9641 + 3dd4b90 commit 013f690
Show file tree
Hide file tree
Showing 49 changed files with 1,853 additions and 246 deletions.
7 changes: 5 additions & 2 deletions apps/course-builder-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@
"@codemirror/lang-markdown": "^6.2.3",
"@codemirror/language-data": "^6.3.1",
"@codemirror/state": "^6.3.3",
"@codemirror/view": "^6.22.3",
"@coursebuilder/ui": "^1.0.1",
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.3.2",
"@mdx-js/loader": "^3.0.0",
"@mdx-js/react": "^3.0.0",
"@msgpack/msgpack": "3.0.0-beta2",
"@mux/mux-player-react": "^2.2.0",
"@next/mdx": "14.0.1",
"@planetscale/database": "^1.11.0",
Expand Down Expand Up @@ -87,7 +89,7 @@
"@types/shortid": "^0.0.31",
"@types/uuid": "^9.0.6",
"@uploadthing/react": "5.7.1-canary.65ab2fd",
"ai": "^2.2.20",
"ai": "^2.2.22",
"aws-sdk": "^2.1483.0",
"axios": "^1.5.1",
"base-64": "^1.0.0",
Expand Down Expand Up @@ -143,7 +145,8 @@
"y-prosemirror": "^1.2.1",
"y-protocols": "^1.0.6",
"yjs": "^13.6.10",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zustand": "^4.4.7"
},
"devDependencies": {
"@mux/mux-node": "^7.3.2",
Expand Down
103 changes: 95 additions & 8 deletions apps/course-builder-web/party/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type * as Party from 'partykit/server'
import {onConnect} from 'y-partykit'
import * as Y from 'yjs'

const BROADCAST_INTERVAL = 1000 / 60 // 60fps

const CORS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept',
}

export default class Server implements Party.Server {
constructor(readonly party: Party.Party) {}
Expand All @@ -13,7 +23,27 @@ export default class Server implements Party.Server {
url: ${new URL(ctx.request.url).pathname}`,
)

return onConnect(conn, this.party, {})
const party = this.party

return onConnect(conn, this.party, {
async load() {
const tip = await sanityQuery<{body: string | null} | null>(
`*[_id == "${party.id}"][0]{body}`,
party.env,
)

const doc = new Y.Doc()
if (tip?.body) {
doc.getText('codemirror').insert(0, tip.body)
}
return doc
},
callback: {
handler: async (doc) => {
// autosave
},
},
})
}

messages: string[] = []
Expand All @@ -22,15 +52,33 @@ export default class Server implements Party.Server {
this.messages = (await this.party.storage.get<string[]>('messages')) ?? []
}

async onRequest(_req: Party.Request) {
const messageBody: {requestId: string; body: string; name: string} =
await _req.json()
async onRequest(req: Party.Request) {
if (req.method === 'GET') {
// For SSR, return the current presence of all connections
// const users = [...this.party.getConnections()].reduce(
// (acc, user) => ({...acc, [user.id]: this.getUser(user)}),
// {},
// )
return Response.json({users: []}, {status: 200, headers: CORS})
}

this.party.broadcast(JSON.stringify(messageBody))
// respond to cors preflight requests
if (req.method === 'OPTIONS') {
return Response.json({ok: true}, {status: 200, headers: CORS})
}

return new Response(
`Party ${this.party.id} has received ${this.messages.length} messages`,
)
if (req.method === 'POST') {
const messageBody: {requestId: string; body: string; name: string} =
await req.json()

this.party.broadcast(JSON.stringify(messageBody))

return new Response(
`Party ${this.party.id} has received ${this.messages.length} messages`,
)
}

return new Response('Method Not Allowed', {status: 405})
}

onMessage(message: string, sender: Party.Connection) {
Expand All @@ -46,3 +94,42 @@ export default class Server implements Party.Server {
}

Server satisfies Party.Worker

export async function sanityQuery<T = any>(
query: string,
env: any,
options: {useCdn?: boolean; revalidate?: number} = {
useCdn: true,
revalidate: 10,
},
): Promise<T> {
return await fetch(
`https://${env.SANITY_STUDIO_PROJECT_ID}.${
options.useCdn ? 'apicdn' : 'api'
}.sanity.io/v${env.SANITY_STUDIO_API_VERSION}/data/query/${
env.SANITY_STUDIO_DATASET || 'production'
}?query=${encodeURIComponent(query)}&perspective=published`,
{
method: 'get',
headers: {
Authorization: `Bearer ${env.SANITY_API_TOKEN}`,
},
next: {revalidate: options.revalidate}, //seconds
},
)
.then(async (response) => {
if (response.status !== 200) {
throw new Error(
`Sanity Query failed with status ${response.status}: ${
response.statusText
}\n\n\n${JSON.stringify(await response.json(), null, 2)})}`,
)
}
const {result} = await response.json()
return result as T
})
.catch((error) => {
console.error('FAAAAAAIIIILLLLL', error)
throw error
})
}
2 changes: 2 additions & 0 deletions apps/course-builder-web/sanity/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import solution from './schemas/objects/solution'
import github from './schemas/objects/github'
import email from './schemas/documents/email'
import imageResource from './schemas/documents/imageResource'
import article from './schemas/documents/article'

export const schema: {types: SchemaTypeDefinition[]} = {
types: [
Expand All @@ -36,6 +37,7 @@ export const schema: {types: SchemaTypeDefinition[]} = {
linkResource,
email,
imageResource,
article,
//objects
solution,
github,
Expand Down
68 changes: 68 additions & 0 deletions apps/course-builder-web/sanity/schemas/documents/article.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {MdArticle} from 'react-icons/md'
import {defineField} from 'sanity'

export default {
name: 'article',
type: 'document',
title: 'Article',
icon: MdArticle,
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'state',
title: 'Current State',
type: 'string',
validation: (Rule) => Rule.required(),
initialValue: 'draft',
options: {
list: [
{title: 'draft', value: 'draft'},
{title: 'published', value: 'published'},
],
},
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
validation: (Rule) => Rule.required(),
options: {
source: 'title',
maxLength: 96,
},
}),
{
name: 'body',
title: 'Body',
type: 'markdown',
},
{
name: 'summary',
title: 'Summary',
type: 'markdown',
},
{
name: 'concepts',
title: 'Concepts',
type: 'array',
of: [
{
type: 'reference',
to: [{type: 'concept'}],
},
],
},
defineField({
name: 'description',
title: 'Short Description',
description: 'Used as a short "SEO" summary on Twitter cards etc.',
type: 'text',
validation: (Rule) => Rule.max(160),
}),
],
}
50 changes: 50 additions & 0 deletions apps/course-builder-web/src/app/[article]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Link from 'next/link'
import {Button} from '@coursebuilder/ui'
import * as React from 'react'
import {getServerAuthSession} from '@/server/auth'
import {getAbility} from '@/lib/ability'
import ReactMarkdown from 'react-markdown'
import {getArticle} from '@/lib/articles'
import {notFound} from 'next/navigation'

export default async function ArticlePage({
params,
}: {
params: {article: string}
}) {
const session = await getServerAuthSession()
const ability = getAbility({user: session?.user})
const article = await getArticle(params.article)

console.log('******', {article})

if (!article) {
notFound()
}

return (
<div>
{ability.can('edit', 'Article') ? (
<div className="flex h-9 w-full items-center justify-between bg-muted px-1">
<div />
<Button asChild className="h-7">
<Link href={`/articles/${article.slug || article._id}/edit`}>
Edit
</Link>
</Button>
</div>
) : null}
<article className="grid grid-cols-5 p-5 sm:p-10">
<div className="col-span-3">
<h1 className="text-3xl font-bold">{article.title}</h1>
</div>
<div className="col-span-2">
<ReactMarkdown className="prose dark:prose-invert">
{article.body}
</ReactMarkdown>
<ReactMarkdown>{article.summary}</ReactMarkdown>
</div>
</article>
</div>
)
}
2 changes: 0 additions & 2 deletions apps/course-builder-web/src/app/_components/chat-response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export function ChatResponse({requestIds = []}: {requestIds: string[]}) {
onMessage: (messageEvent) => {
const messageData = JSON.parse(messageEvent.data)

console.log({requestIds, messageData})

if (
messageData.body !== STREAM_COMPLETE &&
requestIds.includes(messageData.requestId)
Expand Down
1 change: 0 additions & 1 deletion apps/course-builder-web/src/app/_components/landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {Icon} from '@/components/icons'

export const Landing = () => {
const {data: session, status} = useSession()
console.log(session)
return (
<>
<LandingCopy />
Expand Down
5 changes: 2 additions & 3 deletions apps/course-builder-web/src/app/_components/party.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {useSocket} from '@/hooks/use-socket'
import {api} from '@/trpc/react'
import {useRouter} from 'next/navigation'

export function Party() {
export function Party({room}: {room?: string}) {
const utils = api.useUtils()
const router = useRouter()
useSocket({
room,
onMessage: async (messageEvent) => {
try {
const data = JSON.parse(messageEvent.data)
Expand All @@ -18,8 +19,6 @@ export function Party() {
'ai.tip.draft.completed',
]

console.log(data.name)

if (invalidateOn.includes(data.name)) {
await utils.module.invalidate()
router.refresh()
Expand Down
20 changes: 20 additions & 0 deletions apps/course-builder-web/src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ./app/api/chat/route.js
import OpenAI from 'openai'
import {OpenAIStream, StreamingTextResponse} from 'ai'

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})

export const runtime = 'edge'

export async function POST(req: Request) {
const {messages} = await req.json()
const response = await openai.chat.completions.create({
model: 'gpt-4',
stream: true,
messages,
})
const stream = OpenAIStream(response)
return new StreamingTextResponse(stream)
}
21 changes: 21 additions & 0 deletions apps/course-builder-web/src/app/articles/[slug]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react'
import {getServerAuthSession} from '@/server/auth'
import {getAbility} from '@/lib/ability'
import {EditArticleForm} from '@/app/articles/_components/edit-article-form'
import {getArticle} from '@/lib/articles'

export default async function ArticleEditPage({
params,
}: {
params: {slug: string}
}) {
const session = await getServerAuthSession()
const ability = getAbility({user: session?.user})
const article = await getArticle(params.slug)

return article && ability.can('upload', 'Media') ? (
<div className="relative mx-auto flex h-full w-full flex-grow flex-col items-center justify-center">
<EditArticleForm key={article.slug} article={article} />
</div>
) : null
}
Loading

0 comments on commit 013f690

Please sign in to comment.