Skip to content

Commit 0606fba

Browse files
scastielclaude
andcommitted
Go client-side, migrate to Vite, open source (#37)
* Migrate to fully client-side: localStorage + in-browser rendering Rips out every backend system — auth (NextAuth/Resend), Stripe billing, Prisma/Postgres persistence, Remotion Lambda rendering, public snippet routes — and replaces them with: - localStorage snippet storage (src/lib/snippet-storage.ts) - @remotion/web-renderer client-side video generation - No accounts, no plans, no credits, no watermark enforcement Side effects: upgraded Remotion 4.0.68 → 4.0.448, drew background gradients on a <canvas> (web-renderer banned radial-gradient CSS), shimmed Canvas2D fontStretch percentage values, forced dark theme app-wide, replaced the help page with plain TSX (dropped the MDX stack), dropped unused deps (hookform, uuid, lru-cache, react-type-animation). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update CLAUDE.md for client-side architecture Rewrites the doc to describe the post-migration state: localStorage persistence, in-browser renderMediaOnWeb, web-renderer CSS caveats, the forced dark theme (and the useTheme gotcha), and Plausible as the lone third-party service. Also ignores .claude/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove ngrok.yml No longer needed — the Remotion Lambda webhook it tunneled for is gone. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove GitHub workflow update-site.yml re-uploaded the Remotion Lambda site on pushes — Lambda rendering is gone, so the workflow has nothing to do. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Migrate from Next.js to Vite Replace the Next.js App Router with Vite + React SPA and TanStack Router. The app was already fully client-side, so Next was only providing dev server, bundler and routing — Vite gives faster startup, a plain static build, and drops next, next-plausible, next-themes, eslint-config-next. - Static index.html owns the head (OG, Twitter, favicon, fontStretch shim) - Code-based TanStack Router tree in src/routes.tsx - Pages moved from src/app/ to src/pages/; layouts collapse into MyShell - Hardcoded dark theme via <html class="dark"> — no theme provider - import.meta.env.VITE_* replaces NEXT_PUBLIC_* - Jest switched from next/jest to plain ts-jest - package type set to module; CJS configs renamed to .cjs Also refresh the landing page with a "100% free — No account" tag and a new open-source section linking to the repo and GitHub Sponsors. * Add open-source README and GitHub FUNDING config Rewrite the README as a standard OSS readme (features, stack, local setup, scripts, env vars, sponsor link) and add .github/FUNDING.yml so the Sponsor button shows up on the repo. * Add CI workflow and format source with Prettier Run ESLint, Prettier check, and Jest on every push to main and every PR. Also add a format:check script and run prettier --write across src/ so the new check passes from day one. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d27f896 commit 0606fba

163 files changed

Lines changed: 9187 additions & 16986 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
11
{
2-
"extends": "next/core-web-vitals"
2+
"root": true,
3+
"parser": "@typescript-eslint/parser",
4+
"parserOptions": {
5+
"ecmaVersion": 2022,
6+
"sourceType": "module",
7+
"ecmaFeatures": { "jsx": true }
8+
},
9+
"plugins": ["@typescript-eslint"],
10+
"extends": [
11+
"eslint:recommended",
12+
"plugin:@typescript-eslint/recommended"
13+
],
14+
"env": {
15+
"browser": true,
16+
"es2022": true,
17+
"node": true,
18+
"jest": true
19+
},
20+
"ignorePatterns": ["dist", "node_modules", "public/themes"],
21+
"rules": {
22+
"@typescript-eslint/no-explicit-any": "off",
23+
"@typescript-eslint/no-empty-object-type": "off",
24+
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
25+
"no-constant-condition": ["error", { "checkLoops": false }]
26+
}
327
}

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: scastiel

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
check:
10+
name: Lint, format, test
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 20
18+
cache: npm
19+
20+
- run: npm ci
21+
22+
- name: ESLint
23+
run: npm run lint
24+
25+
- name: Prettier
26+
run: npm run format:check
27+
28+
- name: Jest
29+
run: npm test

.github/workflows/update-site.yml

Lines changed: 0 additions & 33 deletions
This file was deleted.

.gitignore

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
# testing
1010
/coverage
1111

12-
# next.js
13-
/.next/
14-
/out/
12+
# vite
13+
/dist/
1514

1615
# production
1716
/build
@@ -34,7 +33,9 @@ yarn-error.log*
3433

3534
# typescript
3635
*.tsbuildinfo
37-
next-env.d.ts
3836

3937
# local postgres data
40-
postgres-data
38+
postgres-data
39+
40+
# Claude Code
41+
.claude/

CLAUDE.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# CLAUDE.md
2+
3+
Guidance for Claude Code when working in this repo.
4+
5+
## Commands
6+
7+
- `npm run dev` — Vite dev server on port 3000
8+
- `npm run build``tsc --noEmit && vite build``dist/`
9+
- `npm start``vite preview` on port 3000 (serves the built `dist/`)
10+
- `npm run lint` — ESLint over `src/**/*.{ts,tsx}`
11+
- `npm test` — Jest (ts-jest, jsdom). Run one test with `npx jest path/to/file.test.ts` or `-t 'name'`
12+
- `npm run remotion-studio` — Remotion Studio pointed at `src/remotion/index.ts` for previewing the composition in isolation
13+
14+
TypeScript path alias: `@/*``src/*`.
15+
16+
## Architecture
17+
18+
**CodeBit** is a Vite + React SPA that turns markdown-defined code snippets into animated videos. It is **fully client-side**: no backend, no database, no accounts, no billing, no SSR. Snippets live in `localStorage`; videos are rendered in the browser via `@remotion/web-renderer` (WebCodecs) and downloaded as MP4. Routing is handled by `@tanstack/react-router` (code-based tree in `src/routes.tsx`).
19+
20+
### The snippet → video pipeline
21+
22+
The markdown snippet format is the central data model. Everything else is a transform over it:
23+
24+
1. **Authoring.** A snippet is markdown with YAML front matter (background, font, watermark, zooming, highlightTheme…) plus fenced code blocks separated by `---`. Each fenced block is one "step" — the diff between consecutive steps drives the typing/replace animation. Templates: the `initialMarkdown()` inside `src/lib/snippet-storage.ts` and `src/lib/tutorial-snippet.ts` / `src/lib/landing-page-snippet.ts`.
25+
2. **Parsing.** `src/lib/code-steps-utils.tsx` (`parseSnippetMardown`) parses front matter with `front-matter`, validates it against `metadataSchema` (Zod, in `src/lib/types.ts`), falls back to defaults for invalid theme/font, and lexes the body with `marked` to produce `Steps` + `Warning[]`. Parse warnings surface in the editor UI via `warning-list.tsx`.
26+
3. **Composition data.** `src/components/remotion/composition-data.ts` turns steps into per-frame data consumed by the Remotion composition (`code-composition.tsx`), including timings. `compositionDurationInFrames` is the source of truth for video length.
27+
4. **Preview vs render.** The same Remotion composition is used two ways, both client-side:
28+
- **Preview** via `@remotion/player` in `src/components/snippet-player.tsx`.
29+
- **MP4 export** via `renderMediaOnWeb()` in `src/components/generate-button.tsx` — produces a Blob via WebCodecs, triggers a download, no network call. `licenseKey: 'free-license'` is passed.
30+
31+
### Web-renderer caveats
32+
33+
`@remotion/web-renderer` (experimental) has strict CSS limits. Don't introduce:
34+
35+
- `radial-gradient` (use `<canvas>` — see `gradient-canvas.tsx` + `gradients.ts:drawGradientOnCanvas`)
36+
- `z-index` (use DOM order instead — the window chrome in `code-composition.tsx` relies on this)
37+
- `clip-path`, `backdrop-filter`, `mix-blend-mode`
38+
- Safari `filter` is broken (pre-accepted)
39+
40+
Canvas2D `fontStretch` setter rejects the `"100%"` value modern browsers return for `font-stretch: normal`. The shim in `src/lib/canvas-shim.ts` (and an inline `<script>` in `index.html` that runs at page parse time) monkey-patches both `CanvasRenderingContext2D.prototype` and `OffscreenCanvasRenderingContext2D.prototype` to silently map any percentage to `'normal'`.
41+
42+
### Persistence
43+
44+
All snippet state lives in `localStorage` under key `codevideo:snippets:v1`. API in `src/lib/snippet-storage.ts`:
45+
46+
```ts
47+
listSnippets(): StoredSnippet[]
48+
getSnippet(slug): StoredSnippet | null
49+
createSnippet(): StoredSnippet // random 6-char hex slug, seeded with initialMarkdown
50+
createTutorialSnippet(): StoredSnippet
51+
updateSnippet(slug, content): void
52+
deleteSnippet(slug): void
53+
```
54+
55+
Preview thumbnails in the snippet list are derived on the fly from the last fenced code block (`derivePreview()` in `snippet-list-item.tsx`) — there is no cached preview column.
56+
57+
### App routes
58+
59+
Routes are declared in `src/routes.tsx` and mounted from `src/main.tsx`:
60+
61+
- `/``src/pages/landing-page.tsx` (plays `landing-page-snippet.ts` via `src/components/landing-player.tsx`)
62+
- `/my` — redirects to `/my/snippets` via `beforeLoad`
63+
- `/my/snippets``src/pages/snippets-page.tsx` (reads from localStorage)
64+
- `/my/snippets/$snippetSlug``src/pages/snippet-editor-page.tsx` (param via `useParams({ from: '/my/snippets/$snippetSlug' })`)
65+
- `/help``src/pages/help-page.tsx` (plain TSX with Tailwind `prose`)
66+
67+
`TopBar` (`src/components/top-bar.tsx`) is rendered inside `MyShell` (`src/components/my-shell.tsx`) by the `/my/*` pages. The help page renders TopBar itself. The landing page uses its own `LandingPageMenu`.
68+
69+
### Theme
70+
71+
The app is **dark-only**. `index.html` sets `<html class="dark">` directly — there is no theme provider. The body has an unconditional `bg-gradient-to-br from-slate-950 to-slate-800` (also applied in `index.html`). Hardcode dark values where needed: `code-editor.tsx` pins Monaco's `github-dark`; `snippet-list.tsx` loads the `github-dark.css` highlight stylesheet.
72+
73+
### UI conventions
74+
75+
shadcn/ui primitives in `src/components/ui/` (config in `components.json`). Tailwind + `tailwind-merge`/`clsx` via `cn()` in `src/lib/utils.ts`. Monaco is the markdown editor (`src/components/code-editor.tsx` wrapping `src/components/markdown-editor.tsx`, themes in `src/lib/monaco-themes.ts`). `ts-pattern` is used for exhaustive pattern matching on tagged unions — prefer it over chained `if`/`switch`.
76+
77+
### Third-party services
78+
79+
Only **Plausible Analytics** — opt-in via `VITE_PLAUSIBLE_DOMAIN` env var. If unset, `src/main.tsx` skips injecting the `<script>` entirely. Events are tracked via `trackEvent()` in `src/lib/plausible.ts` (thin wrapper around `window.plausible`). Everything else is bundled client code.
80+
81+
### Env vars
82+
83+
Parsed by Zod in `src/lib/env.ts` from `import.meta.env`:
84+
85+
- `VITE_BASE_URL` — absolute URL used by the watermark link (default `http://localhost:3000`)
86+
- `VITE_PLAUSIBLE_DOMAIN` — optional; enables Plausible injection if set

README.md

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,65 @@
1-
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
1+
# CodeBit
22

3-
## Getting Started
3+
**Tell a story with your code.** CodeBit turns markdown-defined code snippets into animated videos you can share with your community.
44

5-
First, run the development server:
5+
Try it live at [codebit.xyz](https://codebit.xyz).
6+
7+
![CodeBit banner](public/banner.png)
8+
9+
## Features
10+
11+
- Write code sequences in plain Markdown — no new syntax to learn
12+
- Live preview with typing animations between steps
13+
- Customizable colors, fonts, backgrounds, and highlight themes
14+
- Export as MP4 video, rendered entirely in your browser
15+
- 100% free, no account, no sign-up — everything runs client-side
16+
17+
## Stack
18+
19+
- **Build / dev**: [Vite](https://vitejs.dev/) + [React 18](https://react.dev/)
20+
- **Routing**: [TanStack Router](https://tanstack.com/router) (code-based)
21+
- **Styling**: [Tailwind CSS](https://tailwindcss.com/) + [shadcn/ui](https://ui.shadcn.com/)
22+
- **Editor**: [Monaco](https://microsoft.github.io/monaco-editor/)
23+
- **Video pipeline**: [Remotion](https://www.remotion.dev/)`@remotion/player` for preview, `@remotion/web-renderer` (WebCodecs) for in-browser MP4 export
24+
- **Persistence**: `localStorage` — no backend
25+
- **Tests**: Jest + ts-jest + jsdom
26+
- **Language**: TypeScript
27+
28+
## Run it locally
629

730
```bash
31+
git clone https://github.com/scastiel/codebit.git
32+
cd codebit
33+
npm install
834
npm run dev
9-
# or
10-
yarn dev
11-
# or
12-
pnpm dev
13-
# or
14-
bun dev
1535
```
1636

17-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
37+
The app will be available at [http://localhost:3000](http://localhost:3000).
38+
39+
### Scripts
40+
41+
- `npm run dev` — start the Vite dev server
42+
- `npm run build` — type-check and build to `dist/`
43+
- `npm start` — preview the production build
44+
- `npm run lint` — run ESLint
45+
- `npm test` — run the Jest test suite
46+
- `npm run remotion-studio` — open Remotion Studio to preview the composition in isolation
1847

19-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
48+
### Environment variables
2049

21-
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
50+
Both are optional:
2251

23-
## Learn More
52+
- `VITE_BASE_URL` — absolute URL used by the video watermark (defaults to `http://localhost:3000`)
53+
- `VITE_PLAUSIBLE_DOMAIN` — enables [Plausible Analytics](https://plausible.io/) injection if set
2454

25-
To learn more about Next.js, take a look at the following resources:
55+
## Sponsor
2656

27-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
57+
CodeBit is free and open source. If you enjoy using it, you can [sponsor me on GitHub](https://github.com/sponsors/scastiel) to support its development. ♥
2958

30-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
59+
## License
3160

32-
## Deploy on Vercel
61+
MIT — see [LICENSE](LICENSE) if present, otherwise consider the code MIT-licensed.
3362

34-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
63+
## Credits
3564

36-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
65+
Made with ♥ in Montreal by [@scastiel](https://scastiel.dev) and [@maxday](https://maxday.dev).

index.html

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!doctype html>
2+
<html lang="en" class="dark scroll-smooth">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
7+
<title>Tell a story with your code – CodeBit</title>
8+
<meta
9+
name="description"
10+
content="Create animations from code snippets, and export them as videos to share with your community."
11+
/>
12+
<meta property="og:type" content="website" />
13+
<meta property="og:title" content="Tell a story with your code – CodeBit" />
14+
<meta
15+
property="og:description"
16+
content="Create animations from code snippets, and export them as videos to share with your community."
17+
/>
18+
<meta property="og:image" content="/banner.png" />
19+
<meta name="twitter:card" content="summary_large_image" />
20+
<meta name="twitter:creator" content="@codebitxyz" />
21+
<meta name="twitter:site" content="@codebitxyz" />
22+
<meta name="twitter:title" content="Tell a story with your code – CodeBit" />
23+
<meta
24+
name="twitter:description"
25+
content="Create animations from code snippets, and export them as videos to share with your community."
26+
/>
27+
<meta name="twitter:image" content="/banner.png" />
28+
<script>
29+
(function () {
30+
function patch(proto) {
31+
if (!proto) return
32+
var d = Object.getOwnPropertyDescriptor(proto, 'fontStretch')
33+
if (!d || !d.set) return
34+
var orig = d.set
35+
Object.defineProperty(
36+
proto,
37+
'fontStretch',
38+
Object.assign({}, d, {
39+
set: function (v) {
40+
if (typeof v === 'string' && v.charAt(v.length - 1) === '%') {
41+
orig.call(this, 'normal')
42+
return
43+
}
44+
try {
45+
orig.call(this, v)
46+
} catch (e) {
47+
orig.call(this, 'normal')
48+
}
49+
},
50+
}),
51+
)
52+
}
53+
if (typeof CanvasRenderingContext2D !== 'undefined')
54+
patch(CanvasRenderingContext2D.prototype)
55+
if (typeof OffscreenCanvasRenderingContext2D !== 'undefined')
56+
patch(OffscreenCanvasRenderingContext2D.prototype)
57+
})()
58+
</script>
59+
</head>
60+
<body class="bg-gradient-to-br from-slate-950 to-slate-800">
61+
<div id="root" class="min-h-[100dvh] flex flex-col"></div>
62+
<script type="module" src="/src/main.tsx"></script>
63+
</body>
64+
</html>

jest.config.cjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'jest-environment-jsdom',
5+
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
6+
moduleNameMapper: {
7+
'^@/(.*)$': '<rootDir>/src/$1',
8+
},
9+
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
10+
}

jest.config.js

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)