Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions app/scratchpad/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { MarkdocRenderer } from 'components/markdoc-renderer'
import { getScratchpadEntry } from 'data/scratchpad.dto'
import { getLogger } from 'lib/logger'
import { Metadata } from 'next'
import Link from 'next/link'
import { notFound } from 'next/navigation'

type Params = {
params: Promise<{
slug: string
}>
}

const log = getLogger()
export const generateStaticParams = () => []

export async function generateMetadata(props: Params): Promise<Metadata> {
const { slug } = await props.params
const entryData = await getScratchpadEntry(slug)

if (!entryData) {
log.error(`Scratchpad entry not found for slug: ${slug}`)
return {}
}

const { formattedDate } = entryData
const title = `Scratchpad - ${formattedDate}`

return {
title,
description: 'A scratchpad note',
openGraph: {
title,
description: 'A scratchpad note',
images: [
{
url: 'https://www.chrisjarling.com/og.jpg',
secureUrl: 'https://www.chrisjarling.com/og.jpg',
width: 1200,
height: 630,
},
],
},
}
}

export default async function ScratchpadEntry(props: Params) {
const { slug } = await props.params
const entryData = await getScratchpadEntry(slug)

if (!entryData) {
notFound()
}

const { formattedDate, renderableContent } = entryData

return (
<div className="mx-auto max-w-xl mb-16">
<div className="mb-8">
<Link
href="/scratchpad"
className="text-sm text-base-600 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400"
>
← Back to scratchpad
</Link>
<p className="mt-4 text-sm text-base-600 dark:text-base-400">
{formattedDate}
</p>
</div>
<MarkdocRenderer renderableContent={renderableContent} />
</div>
)
}
53 changes: 53 additions & 0 deletions app/scratchpad/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { LoadMoreButton } from 'components/load-more-button'
import { ScratchpadFeedItem } from 'components/scratchpad-feed-item'
import { getPaginatedScratchpad } from 'data/scratchpad.dto'
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: 'Scratchpad - Chris Jarling',
description: 'Quick notes and thoughts',
}

type SearchParams = Promise<{
page?: string
}>

export default async function ScratchpadPage({
searchParams,
}: {
searchParams: SearchParams
}) {
const params = await searchParams
const page = parseInt(params.page || '1', 10)
const { entries, pagination } = await getPaginatedScratchpad(page)

return (
<div className="max-w-3xl mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Scratchpad</h1>

{entries.length === 0 ? (
<p className="text-base-600 dark:text-base-400">No entries yet.</p>
) : (
<>
<div className="space-y-8">
{entries.map((entry) => (
<ScratchpadFeedItem
key={entry.slug}
slug={entry.slug}
timestamp={entry.entry.timestamp}
content={entry.entry.content}
/>
))}
</div>

{pagination.hasNextPage && (
<LoadMoreButton
nextPage={pagination.currentPage + 1}
basePath="/scratchpad"
/>
)}
</>
)}
</div>
)
}
37 changes: 37 additions & 0 deletions components/__tests__/load-more-button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'

import { LoadMoreButton } from '../load-more-button'

describe('LoadMoreButton', () => {
it('renders a link with the correct href', () => {
render(<LoadMoreButton nextPage={2} basePath="/scratchpad" />)

const link = screen.getByRole('link', { name: /load more/i })
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute('href', '/scratchpad?page=2')
})

it('displays "Load more" text', () => {
render(<LoadMoreButton nextPage={3} basePath="/posts" />)

expect(screen.getByText('Load more')).toBeInTheDocument()
})

it('renders with the correct next page number', () => {
render(<LoadMoreButton nextPage={5} basePath="/articles" />)

const link = screen.getByRole('link', { name: /load more/i })
expect(link).toHaveAttribute('href', '/articles?page=5')
})

it('includes an arrow down icon', () => {
const { container } = render(
<LoadMoreButton nextPage={2} basePath="/scratchpad" />,
)

// ArrowDownIcon should be rendered
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
})
21 changes: 21 additions & 0 deletions components/load-more-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ArrowDownIcon } from '@heroicons/react/24/outline'
import Link from 'next/link'

type LoadMoreButtonProps = {
nextPage: number
basePath: string
}

export function LoadMoreButton({ nextPage, basePath }: LoadMoreButtonProps) {
return (
<div className="flex justify-center mt-8">
<Link
href={`${basePath}?page=${nextPage}`}
className="inline-flex items-center gap-2 px-6 py-3 text-base font-medium text-base-900 dark:text-base-100 bg-base-100 dark:bg-base-800 border border-base-300 dark:border-base-700 rounded-lg hover:bg-base-200 dark:hover:bg-base-700 transition-colors"
>
Load more
<ArrowDownIcon className="w-5 h-5" />
</Link>
</div>
)
}
37 changes: 37 additions & 0 deletions components/scratchpad-feed-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Markdoc, { Node } from '@markdoc/markdoc'
import { format, parseISO } from 'date-fns'
import Link from 'next/link'

import { MarkdocRenderer } from './markdoc-renderer'

type ScratchpadFeedItemProps = {
slug: string
timestamp: string
content: () => Promise<{ node: Node }>
}

export async function ScratchpadFeedItem({
slug,
timestamp,
content,
}: ScratchpadFeedItemProps) {
const parsedTimestamp = parseISO(timestamp)
const formattedDate = format(parsedTimestamp, "do LLL, yyyy 'at' HH:mm")

const { node } = await content()
const renderableContent = Markdoc.transform(node)

return (
<article className="mb-8">
<Link
href={`/scratchpad/${slug}`}
className="text-sm text-base-600 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400"
>
{formattedDate}
</Link>
<div className="mt-2 prose dark:prose-invert">
<MarkdocRenderer renderableContent={renderableContent} />
</div>
</article>
)
}
40 changes: 40 additions & 0 deletions content/scratchpad/basic-ralph-loops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
id: basic-ralph-loops
timestamp: '2026-02-07T15:30:00.000Z'
---

Basic Ralph loops using claude:

Setup:

- plan.md: a plan of what to build, for reference
- tasks.toon: a list of small tasks that agents can work on
- progress.txt: cross-agent memory
- prompot.md: The prompt passed into claude

Basic loop:

```bash
while true; cat plans/prompt.md | claude --permission-mode acceptEdits; end
```

with prompt.md

```
- Read tasks.toon
- Read progress.txt
- Familiarize yourself with the codebase.

Afterwards, pick the most important next thing to work in in the tasks.md file.
If all work is done or you require input to keep working on any task, notify the user.

IMPORTANT:

- If there are no tests for a feature yet, write them first and run them to ensure they fail
- After doing your work, ensure tests are passing
- Only work at one task at a time
- After finishing a taks, add a note to plans/progress.txt
- After you are done with one task, you must shut this instance of yourself down. You are being run in a loop and a fresh instance will spawn right away to work on the next task. Get your own pid and end it so the next iteration can start with a fresh context.
```

Works well, but instances don't shut down automatically, have to close manually after every task. `-p` flag works better, but lose live output to examine. With `-p`, also want max iterations likely.
Loading
Loading