Last active
September 7, 2024 10:02
-
-
Save longseespace/7adb9e27d0dba2848a6496e76e75454e to your computer and use it in GitHub Desktop.
BoltAI.com Changelog Page (NextJS on Cloudflare Pages, data pulled from Canny)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CANNY_API_KEY=<your_canny_api_key> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Head from 'next/head' | |
import { Container } from '@/components/Container' | |
import { Header } from '@/components/Header' | |
import { Footer } from '@/components/Footer' | |
import { format } from 'date-fns' | |
import Markdown from 'react-markdown' | |
import rehypeRaw from 'rehype-raw' | |
export const runtime = 'experimental-edge' | |
export const getServerSideProps = async ({ req, res, query }) => { | |
const apiKey = process.env.CANNY_API_KEY | |
const page = Math.max(query.page || 1, 1) | |
const skip = (page - 1) * 10 | |
const response = await fetch( | |
`https://canny.io/api/v1/entries/list?skip=${skip}`, | |
{ | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
apiKey: apiKey, | |
}), | |
} | |
) | |
const data = await response.json() | |
return { | |
props: { | |
data, | |
page, | |
}, | |
} | |
} | |
function convertToIframe(markdownContent) { | |
let updatedContent = markdownContent | |
// Regular expression to match YouTube video links | |
const youtubeRegex = /!\[([^\]]*)\]\(https:\/\/youtu\.be\/([^\)]+)\)/g | |
// Replace YouTube links with embedded iframes | |
updatedContent = updatedContent.replace( | |
youtubeRegex, | |
(match, title, videoId) => { | |
return `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" title="${title}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>` | |
} | |
) | |
return updatedContent | |
} | |
export default function ChangelogPage({ data, page }) { | |
const entries = data.entries.filter((entry) => entry.status === 'published') | |
const hasMore = data.hasMore | |
const hasPrev = page > 1 | |
const prevPageLink = `/changelog?page=${page - 1}` | |
const nextPageLink = `/changelog?page=${page + 1}` | |
return ( | |
<> | |
<Head> | |
<title>Changelog | BoltAI</title> | |
<meta | |
property="og:image" | |
content="https://image.social/get?url=boltai.com/changelog" | |
/> | |
</Head> | |
<Header /> | |
<main> | |
<Container> | |
<article className="prose lg:prose-xl overflow-hidden py-20 sm:py-32 lg:pb-32 xl:py-32 min-h-screen"> | |
<h1>Changelog</h1> | |
{entries.map((entry) => ( | |
<div key={entry.id}> | |
<span className="text-sm font-semibold text-gray-500"> | |
{format(new Date(entry.publishedAt), 'MMMM do, yyyy')} | |
</span> | |
{/* I'm sorry for this :( */} | |
<h2 className="!mt-0 !mb-2">{entry.title}</h2> | |
<div className="flex gap-2 mt-4"> | |
{entry.types.includes('new') && ( | |
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20"> | |
New | |
</span> | |
)} | |
{entry.types.includes('improved') && ( | |
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/20"> | |
Improved | |
</span> | |
)} | |
{entry.types.includes('fixed') && ( | |
<span className="inline-flex items-center rounded-md bg-purple-50 px-2 py-1 text-xs font-medium text-purple-700 ring-1 ring-inset ring-purple-600/20"> | |
Fixed | |
</span> | |
)} | |
</div> | |
<Markdown rehypePlugins={[rehypeRaw]}> | |
{convertToIframe(entry.markdownDetails)} | |
</Markdown> | |
<hr /> | |
</div> | |
))} | |
<div className="flex gap-3"> | |
{hasPrev && ( | |
<a href={prevPageLink} className="text-sm text-blue-600"> | |
← Previous Page | |
</a> | |
)} | |
{hasMore && ( | |
<a href={nextPageLink} className="text-sm text-blue-600"> | |
Next Page → | |
</a> | |
)} | |
</div> | |
</article> | |
</Container> | |
</main> | |
<Footer /> | |
</> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment