Skip to content

Commit

Permalink
feat(templates): added Beehiiv & Substack
Browse files Browse the repository at this point in the history
  • Loading branch information
waptik committed Sep 23, 2024
1 parent 4256efe commit 327ddce
Show file tree
Hide file tree
Showing 21 changed files with 1,827 additions and 0 deletions.
197 changes: 197 additions & 0 deletions templates/beehiiv/Inspector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
'use client'
import { ColorPicker, Input } from '@/sdk/components'
import { useFrameConfig } from '@/sdk/hooks'
import { Configuration } from '@/sdk/inspector'
import { LoaderIcon } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { Config } from '.'
import getBeehiivArticle from './utils'

export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
const [loading, setLoading] = useState(false)

const urlInputRef = useRef<HTMLInputElement>(null)
const imgSizeInputRef = useRef<HTMLInputElement>(null)
const pagesFontSizeInputRef = useRef<HTMLInputElement>(null)
const textPositionOverlayRef = useRef<HTMLInputElement>(null)
const linkOnAllPagesRef = useRef<HTMLInputElement>(null)
const hideTitleAuthorRef = useRef<HTMLInputElement>(null)

// keep the url input updated with the article URL
useEffect(() => {
if (!urlInputRef.current) return
if (!urlInputRef.current.value) return

urlInputRef.current.value = config.article?.url ?? ''
}, [config.article])

// handler for the article URL input

const urlInputHandler = async (url: string) => {
if (url === config.article?.url) return
if (url === '') {
updateConfig({ article: null })
return
}

if (!/^(https?:\/\/[^\s]+)/.test(url)) {
toast.error('Please enter a valid beehiv article URL')
return
}

try {
setLoading(true)
const newArticle = await getBeehiivArticle(url)
updateConfig({ article: newArticle })
console.log(newArticle)
toast.success('Successfully fetched the beehiv article')
} catch (e) {
console.error('beehiiv', e)
toast.error('Please enter a valid beehiv article URL')
} finally {
setLoading(false)
}
}

return (
<Configuration.Root>
<Configuration.Section title="Beehiiv Article URL">
<div className="flex flex-row w-full gap-2">
<Input
className="py-2 text-lg"
placeholder="https://blog.beehiv.com/..."
ref={urlInputRef}
defaultValue={config.article?.url ?? ''}
onChange={(e) => urlInputHandler(e.target.value)}
/>
{loading && <LoaderIcon className="animate-spin" />}
</div>
</Configuration.Section>
<Configuration.Section title="Cover Options">
<label className="flex gap-x-4 items-center">
<Input
className="w-4 h-4"
name="textPosition"
type="radio"
defaultChecked={!config.textPosition}
onChange={() =>
updateConfig({
textPosition: textPositionOverlayRef.current?.checked,
})
}
/>
<p className="text-lg">Place the text below the image</p>
</label>
<label className="flex gap-x-4 items-center">
<Input
className="w-4 h-4"
name="textPosition"
type="radio"
defaultChecked={config.textPosition}
ref={textPositionOverlayRef}
onChange={() =>
updateConfig({
textPosition: textPositionOverlayRef.current?.checked,
})
}
/>
<p className="text-lg">Place the text over the image</p>
</label>
<label className="mt-4 flex gap-x-4 items-center">
<Input
className="w-4 h-4"
defaultChecked={config.hideTitleAuthor}
type="checkbox"
ref={hideTitleAuthorRef}
onChange={() =>
updateConfig({
hideTitleAuthor: hideTitleAuthorRef.current?.checked,
})
}
/>
<h2 className="text-lg">Hide the Title & Author</h2>
</label>
<label className="mt-1 flex gap-x-4 items-center">
<Input
className="w-4 h-4"
type="checkbox"
defaultChecked={config.showLinkOnAllPages}
ref={linkOnAllPagesRef}
onChange={() =>
updateConfig({
showLinkOnAllPages: linkOnAllPagesRef.current?.checked,
})
}
/>
<div className="flex flex-col">
<h2 className="text-lg">Show link to article on every frame</h2>
<p className="text-sm text-gray-400">uncheck for last page only</p>
</div>
</label>
</Configuration.Section>
<Configuration.Section title="Cover Styles">
<div className="flex flex-col gap-2">
<h2 className="text-lg">Background Color</h2>
<ColorPicker
className="w-full"
background={config.coverBgColor || 'black'}
setBackground={(value) => updateConfig({ coverBgColor: value })}
/>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-lg">Text Color</h2>
<ColorPicker
className="w-full"
background={config.coverTextColor || 'white'}
setBackground={(value) => updateConfig({ coverTextColor: value })}
/>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-lg">Image Size</h2>
<Input
className="w-full"
type="number"
defaultValue={config.imageSize ?? 40}
ref={imgSizeInputRef}
onBlur={() => updateConfig({ imageSize: imgSizeInputRef.current?.value })}
/>
<p className="text-sm text-gray-400">
Enter a percent value and test (frame size limit is 256kb!)
</p>
</div>
</Configuration.Section>
<Configuration.Section title="Page Styles">
<div className="flex flex-col gap-2">
<h2 className="text-lg">Background Color</h2>
<ColorPicker
className="w-full"
background={config.pagesBgColor || 'white'}
setBackground={(value) => updateConfig({ pagesBgColor: value })}
/>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-lg">Text Color</h2>
<ColorPicker
className="w-full"
background={config.pagesTextColor || 'black'}
setBackground={(value) => updateConfig({ pagesTextColor: value })}
/>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-lg">Font Size</h2>
<Input
className="w-full"
type="number"
defaultValue={config.pagesFontSize ?? 18}
ref={pagesFontSizeInputRef}
onBlur={() =>
updateConfig({ pagesFontSize: pagesFontSizeInputRef.current?.value })
}
/>
</div>
</Configuration.Section>
</Configuration.Root>
)
}
Binary file added templates/beehiiv/cover.avif
Binary file not shown.
7 changes: 7 additions & 0 deletions templates/beehiiv/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import initial from './initial'
import page from './page'

export default {
initial,
page,
}
28 changes: 28 additions & 0 deletions templates/beehiiv/handlers/initial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use server'
import type { BuildFrameData } from '@/lib/farcaster'
import { loadGoogleFontAllVariants } from '@/sdk/fonts'
import type { Config } from '..'
import CoverView from '../views/Cover'

export default async function initial({ config }: { config: Config }): Promise<BuildFrameData> {
const georgia = await loadGoogleFontAllVariants('Georgia')

return {
buttons: [
{
label: 'Read →',
},
],
aspectRatio: '1:1',
fonts: georgia,
component: CoverView({
article: config.article,
bgColor: config.coverBgColor,
textColor: config.coverTextColor,
imageSize: config.imageSize,
textPosition: config.textPosition,
hideTitleAuthor: config.hideTitleAuthor,
}),
handler: 'page',
}
}
72 changes: 72 additions & 0 deletions templates/beehiiv/handlers/page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use server'
import type { BuildFrameData, FrameButtonMetadata, FramePayloadValidated } from '@/lib/farcaster'
import { loadGoogleFontAllVariants } from '@/sdk/fonts'
import type { Config } from '..'
import PageView from '../views/Page'
import initial from './initial'

export default async function page({
body,
config,
params,
}: {
body: FramePayloadValidated
config: Config
params: any
}): Promise<BuildFrameData> {
const nextPage =
params?.currentPage !== undefined
? body.tapped_button.index === 1
? Number(params?.currentPage) - 1
: Number(params?.currentPage) + 1
: 1

const slideCount = (config.article?.pages ?? []).length || 1

//console.log('slide count', slideCount)

const buttons: FrameButtonMetadata[] = [
{
label: '←',
},
]

if (nextPage < slideCount) {
buttons.push({
label: '→',
})
}

if (config.article?.url && (config.showLinkOnAllPages || nextPage === slideCount)) {
buttons.push({
label: 'Beehiiv',
action: 'link',
target: config.article.url,
})
}

if (body.tapped_button.index === 1 && nextPage === 0) {
return initial({ config })
}

const page = config.article?.pages[nextPage - 1]
const georgia = await loadGoogleFontAllVariants('Georgia')

return {
buttons: buttons,
aspectRatio: '1:1',
fonts: georgia,
component: PageView({
page: page || [],
currentPage: nextPage,
slideCount: slideCount,
pagesBgColor: config.pagesBgColor,
pagesTextColor: config.pagesTextColor,
pagesFontSize: config.pagesFontSize,
}),
handler: 'page',
params: {
currentPage: nextPage,
},
}
}
Binary file added templates/beehiiv/icon.avif
Binary file not shown.
37 changes: 37 additions & 0 deletions templates/beehiiv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { BaseConfig, BaseStorage, BaseTemplate } from '@/lib/types'
import Inspector from './Inspector'
import cover from './cover.avif'
import handlers from './handlers'
import icon from './icon.avif'
import type { BeehiivArticle } from './utils'

export interface Config extends BaseConfig {
article?: BeehiivArticle
coverBgColor: string
coverTextColor: string
imageSize: number
textPosition: boolean
showLinkOnAllPages: boolean
hideTitleAuthor: boolean
pagesBgColor: string
pagesTextColor: string
pagesFontSize: number
}

export interface Storage extends BaseStorage {}

export default {
name: 'Beehiiv',
description: 'Convert any Beehiiv article into a Farcaster Frame.',
shortDescription: 'Beehiiv article to Frame',
octicon: 'log',
icon: icon,
creatorFid: '260812',
creatorName: 'Steve',
enabled: true,
Inspector,
handlers,
cover,
initialConfig: {},
events: [],
} satisfies BaseTemplate
Loading

0 comments on commit 327ddce

Please sign in to comment.