Skip to content

Commit

Permalink
feat(ai): scrollycoding mdx (#366)
Browse files Browse the repository at this point in the history
* feat(ai): add basic scrollycoding components from codehike

* feat(ai): add basic scrollycoding components from codehike

* feat(ai): add basic scrollycoding components from codehike

* feat(ai): add basic scrollycoding components from codehike

* feat(ai): add basic scrollycoding components from codehike

* feat(ai): add basic scrollycoding components from codehike

* Merge branch 'main' of https://github.com/skillrecordings/course-builder into vh/feat/ai/scrollycoding

* feat(ai): add basic scrollycoding components from codehike

* feat(ai): add basic scrollycoding components from codehike

* feat(ai): add basic scrollycoding components from codehike
  • Loading branch information
vojtaholik authored Jan 6, 2025
1 parent cb911e5 commit 6a799d1
Show file tree
Hide file tree
Showing 19 changed files with 542 additions and 77 deletions.
5 changes: 1 addition & 4 deletions apps/ai-hero/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import { withAxiom } from 'next-axiom'
await import('./src/env.mjs')

const withMDX = createMDX({
options: {
remarkPlugins: [],
rehypePlugins: [],
},
options: {},
})

/** @type {import("next").NextConfig} */
Expand Down
1 change: 1 addition & 0 deletions apps/ai-hero/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"cloudinary": "^1.41.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"codehike": "^1.0.4",
"codemirror": "^6.0.1",
"crypto": "^1.0.1",
"date-fns": "^2.30.0",
Expand Down
100 changes: 50 additions & 50 deletions apps/ai-hero/src/app/(content)/[post]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Link from 'next/link'
import { notFound } from 'next/navigation'
import { Contributor } from '@/app/_components/contributor'
import { PricingWidget } from '@/app/_components/home-pricing-widget'
import { Code } from '@/components/codehike/code'
import Scrollycoding from '@/components/codehike/scrollycoding'
import { PlayerContainerSkeleton } from '@/components/player-skeleton'
import { PrimaryNewsletterCta } from '@/components/primary-newsletter-cta'
import { Share } from '@/components/share'
Expand All @@ -17,9 +19,9 @@ import { getServerAuthSession } from '@/server/auth'
import { cn } from '@/utils/cn'
import { generateGridPattern } from '@/utils/generate-grid-pattern'
import { getOGImageUrlForResource } from '@/utils/get-og-image-url-for-resource'
import { codeToHtml } from '@/utils/shiki'
import { CK_SUBSCRIBER_KEY } from '@skillrecordings/config'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { recmaCodeHike, remarkCodeHike } from 'codehike/mdx'
import { compileMDX, MDXRemote } from 'next-mdx-remote/rsc'
import remarkGfm from 'remark-gfm'

import { Button } from '@coursebuilder/ui'
Expand Down Expand Up @@ -86,38 +88,40 @@ async function Post({ post }: { post: Post | null }) {
return null
}

return (
<article className="prose sm:prose-lg lg:prose-xl prose-p:text-foreground/80 mt-10 max-w-none">
{post.fields.body && (
<MDXRemote
source={post.fields.body}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
if (!post.fields.body) {
return null
}

const { content } = await compileMDX({
source: post.fields.body,
// @ts-expect-error
components: { Code, Scrollycoding },
options: {
mdxOptions: {
remarkPlugins: [
remarkGfm,
[
remarkCodeHike,
{
components: { code: 'Code' },
},
}}
components={{
// @ts-expect-error
pre: async (props: any) => {
const children = props?.children.props.children
const language =
props?.children.props.className?.split('-')[1] || 'typescript'
try {
const html = await codeToHtml({ code: children, language })
return (
<div
className="before:via-foreground/10 relative rounded before:absolute before:left-0 before:top-0 before:h-px before:w-full before:bg-gradient-to-r before:from-transparent before:to-transparent"
dangerouslySetInnerHTML={{ __html: html }}
/>
)
} catch (error) {
console.error(error)
return <pre {...props} />
}
],
],
recmaPlugins: [
[
recmaCodeHike,
{
components: { code: 'Code' },
},
}}
/>
)}
],
],
},
},
})

return (
<article className="prose sm:prose-lg lg:prose-xl prose-p:max-w-4xl prose-headings:max-w-4xl prose-ul:max-w-4xl prose-table:max-w-4xl prose-pre:max-w-4xl prose-p:text-foreground/80 mt-10 max-w-none">
{content}
</article>
)
}
Expand Down Expand Up @@ -168,8 +172,9 @@ export default async function PostPage(props: {
<main>
{hasVideo && <PlayerContainer post={post} />}
<div
className={cn('container relative max-w-screen-xl pb-24', {
className={cn('container relative max-w-screen-xl pb-16 sm:pb-24', {
'pt-16': !hasVideo,
// 'pb-24': ckSubscriber || hasVideo,
})}
>
<div
Expand Down Expand Up @@ -208,13 +213,18 @@ export default async function PostPage(props: {
</div>
</div>
<div className="relative z-10">
<article className="flex h-full grid-cols-12 flex-col gap-5 md:grid">
<div className="col-span-8">
<PostTitle post={post} />
<Contributor className="flex md:hidden [&_img]:w-8" />
<Post post={post} />
<article className="flex h-full flex-col gap-5">
<PostTitle post={post} />
<Contributor className="flex [&_img]:w-8" />
<Post post={post} />
<div className="mx-auto mt-10 flex w-full max-w-sm flex-col gap-1">
<strong className="text-lg font-semibold">Share</strong>
<Share
className="bg-background w-full"
title={post?.fields.title}
/>
</div>
<aside className="relative col-span-3 col-start-10 flex h-full flex-col pt-24">
{/* <aside className="relative col-span-3 col-start-10 flex h-full flex-col pt-24">
<div className="top-20 md:sticky">
<Contributor className="hidden md:flex" />
<div className="mt-5 flex w-full flex-col gap-1">
Expand All @@ -224,18 +234,8 @@ export default async function PostPage(props: {
title={post?.fields.title}
/>
</div>
{/* <div className="relative">
<img
src={squareGridPattern}
className="my-2 h-[30px] w-[289px] overflow-hidden object-cover object-left-top opacity-75 saturate-0"
/>
<div
className="to-background absolute left-0 top-0 z-10 h-full w-full bg-gradient-to-r from-transparent"
aria-hidden="true"
/>
</div> */}
</div>
</aside>
</aside> */}
</article>
</div>
</div>
Expand Down
34 changes: 34 additions & 0 deletions apps/ai-hero/src/components/codehike/callout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react'
import { AnnotationHandler, InlineAnnotation } from 'codehike/code'

export const callout: AnnotationHandler = {
name: 'callout',
transform: (annotation: InlineAnnotation) => {
const { name, query, lineNumber, fromColumn, toColumn, data } = annotation
return {
name,
query,
fromLineNumber: lineNumber,
toLineNumber: lineNumber,
data: { ...data, column: (fromColumn + toColumn) / 2 },
}
},
Block: ({ annotation, children }) => {
const { column } = annotation.data
return (
<>
{children}
<div
style={{ minWidth: `${column + 4}ch` }}
className="relative -ml-[1ch] mt-1 w-fit whitespace-break-spaces rounded border border-current bg-zinc-800 px-2"
>
<div
style={{ left: `${column}ch` }}
className="absolute -top-[1px] h-2 w-2 -translate-y-1/2 rotate-45 border-l border-t border-current bg-zinc-800"
/>
{annotation.query}
</div>
</>
)
},
}
16 changes: 16 additions & 0 deletions apps/ai-hero/src/components/codehike/code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'
import { highlight, Pre, RawCode } from 'codehike/code'

import { callout, diff, focus, fold, link, mark } from './handlers'

export async function Code({ codeblock }: { codeblock: RawCode }) {
const highlighted = await highlight(codeblock, 'github-dark')
return (
<Pre
code={highlighted}
className="bg-background text-xs sm:text-sm"
style={{ ...highlighted.style, padding: '1rem', borderRadius: '0.5rem' }}
handlers={[callout, fold, mark, diff, focus, link]}
/>
)
}
19 changes: 19 additions & 0 deletions apps/ai-hero/src/components/codehike/diff.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import { AnnotationHandler, BlockAnnotation, InnerLine } from 'codehike/code'

export const diff: AnnotationHandler = {
name: 'diff',
onlyIfAnnotated: true,
transform: (annotation: BlockAnnotation) => {
const color = annotation.query == '-' ? '#f85149' : '#3fb950'
return [annotation, { ...annotation, name: 'mark', query: color }]
},
Line: ({ annotation, ...props }) => (
<>
<div className="box-content min-w-[1ch] select-none pl-2 opacity-70">
{annotation?.query}
</div>
<InnerLine merge={props} />
</>
),
}
41 changes: 41 additions & 0 deletions apps/ai-hero/src/components/codehike/focus.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client'

import React, { useLayoutEffect, useRef } from 'react'
import { AnnotationHandler, getPreRef, InnerPre } from 'codehike/code'

export const PreWithFocus: AnnotationHandler['PreWithRef'] = (props) => {
const ref = getPreRef(props)
useScrollToFocus(ref)
return <InnerPre merge={props} />
}

function useScrollToFocus(ref: React.RefObject<HTMLPreElement>) {
const firstRender = useRef(true)
useLayoutEffect(() => {
if (ref.current) {
// find all descendants whith data-focus="true"
const focusedElements = ref.current.querySelectorAll(
'[data-focus=true]',
) as NodeListOf<HTMLElement>

// find top and bottom of the focused elements
const containerRect = ref.current.getBoundingClientRect()
let top = Infinity
let bottom = -Infinity
focusedElements.forEach((el) => {
const rect = el.getBoundingClientRect()
top = Math.min(top, rect.top - containerRect.top)
bottom = Math.max(bottom, rect.bottom - containerRect.top)
})

// scroll to the focused elements if any part of them is not visible
if (bottom > containerRect.height || top < 0) {
ref.current.scrollTo({
top: ref.current.scrollTop + top - 10,
behavior: firstRender.current ? 'instant' : 'smooth',
})
}
firstRender.current = false
}
})
}
19 changes: 19 additions & 0 deletions apps/ai-hero/src/components/codehike/focus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import { AnnotationHandler, InnerLine } from 'codehike/code'

import { PreWithFocus } from './focus.client'

export const focus: AnnotationHandler = {
name: 'focus',
onlyIfAnnotated: true,
PreWithRef: PreWithFocus,
Line: (props) => (
<InnerLine
merge={props}
className="px-2 opacity-50 data-[focus]:opacity-100"
/>
),
AnnotatedLine: ({ annotation, ...props }) => (
<InnerLine merge={props} data-focus={true} className="bg-zinc-700/30" />
),
}
21 changes: 21 additions & 0 deletions apps/ai-hero/src/components/codehike/fold.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'

import React, { useState } from 'react'
import { AnnotationHandler } from 'codehike/code'

const InlineFold: AnnotationHandler['Inline'] = ({ children }) => {
const [folded, setFolded] = useState(true)
if (!folded) {
return children
}
return (
<button onClick={() => setFolded(false)} aria-label="Expand">
...
</button>
)
}

export const fold: AnnotationHandler = {
name: 'fold',
Inline: InlineFold,
}
8 changes: 8 additions & 0 deletions apps/ai-hero/src/components/codehike/handlers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { callout } from './callout'
import { diff } from './diff'
import { focus } from './focus'
import { fold } from './fold'
import { link } from './link'
import { mark } from './mark'

export { callout, diff, fold, mark, focus, link }
10 changes: 10 additions & 0 deletions apps/ai-hero/src/components/codehike/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import { AnnotationHandler } from 'codehike/code'

export const link: AnnotationHandler = {
name: 'link',
Inline: ({ annotation, children }) => {
const { query } = annotation
return <a href={query}>{children}</a>
},
}
35 changes: 35 additions & 0 deletions apps/ai-hero/src/components/codehike/mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { AnnotationHandler, InnerLine } from 'codehike/code'

export const mark: AnnotationHandler = {
name: 'mark',
Line: ({ annotation, ...props }) => {
const color = annotation?.query || 'rgb(14 165 233)'
return (
<div
className="flex"
style={{
borderLeft: 'solid 2px transparent',
borderLeftColor: annotation && color,
backgroundColor: annotation && `rgb(from ${color} r g b / 0.1)`,
}}
>
<InnerLine merge={props} className="flex-1 px-2" />
</div>
)
},
Inline: ({ annotation, children }) => {
const color = annotation?.query || 'rgb(14 165 233)'
return (
<span
className="-mx-0.5 rounded px-0.5 py-0"
style={{
outline: `solid 1px rgb(from ${color} r g b / 0.5)`,
background: `rgb(from ${color} r g b / 0.13)`,
}}
>
{children}
</span>
)
},
}
Loading

0 comments on commit 6a799d1

Please sign in to comment.