Educational platforms where you pick the better option in side-by-side comparisons, covering code patterns, design, chemistry, and more.
| App | Topic | Tool | Live |
|---|---|---|---|
| cant-maintain | React component API design | Changelog | cant-maintain.saschb2b.com |
| cant-resize | Responsive design patterns | Multi-device viewer | cant-resize.saschb2b.com |
| cant-type | TypeScript patterns | TS Playground | cant-type.saschb2b.com |
| cant-orchestrate | Container orchestration | Dockerfile explorer | cant-orchestrate.saschb2b.com |
| cant-seo | SEO for Next.js | Link inspector | cant-seo.saschb2b.com |
| cant-ux | UX design patterns | Visual comparisons | cant-ux.saschb2b.com |
| cant-explode | Chemistry and biochemistry | 3D molecule viewer | cant-explode.saschb2b.com |
| cant-hub | Series hub / landing | App directory | cant.saschb2b.com |
- Framework: Next.js 16 (App Router, Turbopack)
- UI: MUI 7, Emotion, Lucide icons
- Syntax highlighting: Shiki
- 3D molecular viewer: 3Dmol.js (cant-explode)
- Monorepo: pnpm workspaces + Turborepo
- Shared components: Storybook 10
- Analytics: Umami (self-hosted)
- Hosting: Coolify (self-hosted, Docker)
corepack enable
corepack prepare pnpm@10.20.0 --activate# Install all dependencies
pnpm install
# Start all apps at once (cross-app links work between them)
pnpm dev
# Or start a single app
pnpm dev:hub # cant-hub on :3000
pnpm dev:maintain # cant-maintain on :3001
pnpm dev:resize # cant-resize on :3002
pnpm dev:type # cant-type on :3003
pnpm dev:orchestrate # cant-orchestrate on :3004
pnpm dev:seo # cant-seo on :3005
pnpm dev:ux # cant-ux on :3006
pnpm dev:explode # cant-explode on :3007
# Start Storybook for shared components
pnpm storybook # opens on :6006Each app has a fixed dev port. When running pnpm dev, all apps start simultaneously and cross-app links (header, footer, series grid) automatically point to localhost:<port> instead of the production URLs.
cant/
├── apps/
│ ├── cant-maintain/ # React API patterns app
│ ├── cant-resize/ # Responsive design app
│ ├── cant-type/ # TypeScript patterns app
│ ├── cant-orchestrate/ # Container orchestration app
│ ├── cant-seo/ # SEO patterns app
│ ├── cant-ux/ # UX design patterns app
│ ├── cant-explode/ # Chemistry and biochemistry app
│ └── cant-hub/ # Series hub / landing page
├── packages/
│ └── shared/ # @cant/shared — shared components and utils
│ ├── .storybook/ # Storybook config
│ └── src/
│ ├── components/ # UI components
│ └── lib/ # Utilities, hooks, game logic
├── docs/ # Deployment and ops documentation
├── turbo.json # Turborepo task config
├── pnpm-workspace.yaml # Workspace definition
└── tsconfig.base.json # Shared TypeScript config
Run from the repo root:
| Script | Description |
|---|---|
pnpm dev |
Start all apps with cross-app linking |
pnpm dev:hub |
Start cant-hub only (:3000) |
pnpm dev:maintain |
Start cant-maintain only (:3001) |
pnpm dev:resize |
Start cant-resize only (:3002) |
pnpm dev:type |
Start cant-type only (:3003) |
pnpm dev:orchestrate |
Start cant-orchestrate only (:3004) |
pnpm dev:seo |
Start cant-seo only (:3005) |
pnpm dev:ux |
Start cant-ux only (:3006) |
pnpm dev:explode |
Start cant-explode only (:3007) |
pnpm build |
Production build all apps (parallel) |
pnpm build:maintain |
Build cant-maintain only |
pnpm build:resize |
Build cant-resize only |
pnpm build:type |
Build cant-type only |
pnpm build:orchestrate |
Build cant-orchestrate only |
pnpm build:seo |
Build cant-seo only |
pnpm build:ux |
Build cant-ux only |
pnpm build:hub |
Build cant-hub only |
pnpm build:explode |
Build cant-explode only |
pnpm lint |
Lint all apps |
pnpm typecheck |
Type-check all apps |
pnpm format:check |
Check formatting |
pnpm storybook |
Launch Storybook for shared package |
pnpm build-storybook |
Build static Storybook |
The packages/shared package contains components and utilities used across all apps. Apps import from it using:
import { ThemeProvider } from "@cant/shared/components/theme-provider";
import { CantSeriesGrid } from "@cant/shared/components/cant-series-grid";
import { createTracker } from "@cant/shared/lib/analytics";Components: ThemeProvider, EmotionRegistry, FormattedText, ChallengeAnchor, SourceLink, Template, NotFound, AnalyticsProviderWrapper, CantSeriesGrid, Hero, HeroCta, FeatureGrid, OpenSourceBanner
Game UI: Game, GameHeader, LobbyScreen, ResultsScreen, CodePanel, ImagePanel, VisualPanel, ExplanationPanel, ActivityGraph, CategoryFilter, SeedInput
Learn UI: LearnIndexPage, LearnCategoryPage, LearnContentPanel, LearnSidebar, LearnMobileNav
Utilities: Shiki highlighter, code block styles, analytics context, app theme context, game types, activity store, history store, seeded random, app registry
Per-app customization (panel labels, styling, checkmark animations) is centralized via an AppThemeProvider context. Each app defines its config once in a wrapper component, and all shared components read from it via useAppTheme().
packages/shared/src/lib/
├── app-theme.ts # Types, defaults, createAppTheme() — importable by server components
└── app-theme-context.tsx # "use client" — context, provider, hook
Each app creates components/app-theme-wrapper.tsx:
"use client";
import {
AppThemeProvider,
createAppTheme,
} from "@cant/shared/lib/app-theme-context";
import checkmarkAnimation from "./game/checkmark-animation.json";
const appTheme = createAppTheme({
labels: { betterLabel: "Correct", worseLabel: "Incorrect" },
styling: { headerBackground: "secondary.main" },
slots: { checkmarkAnimation },
});
export function AppThemeWrapper({ children }) {
return <AppThemeProvider value={appTheme}>{children}</AppThemeProvider>;
}And wraps it in app/layout.tsx:
<ThemeProvider theme={theme}>
<AnalyticsProviderWrapper>
<AppThemeWrapper>{children}</AppThemeWrapper>
</AnalyticsProviderWrapper>
</ThemeProvider>For server components (learn pages), import from the non-client module:
import { createAppTheme } from "@cant/shared/lib/app-theme";Configurable values:
| Field | Type | Default | Used by |
|---|---|---|---|
labels.betterLabel |
ReactNode |
"Better" |
Game panels (correct answer) |
labels.worseLabel |
ReactNode |
"Worse" |
Game panels (wrong answer) |
labels.badLabel |
string |
"Avoid" |
Learn pages (bad side header) |
labels.goodLabel |
string |
"Prefer" |
Learn pages (good side header) |
styling.headerBackground |
string |
"action.selected" |
Panel headers, lobby cards |
styling.codeBackground |
string |
"background.paper" |
Code panel background |
slots.checkmarkAnimation |
JSON |
undefined |
Lottie checkmark on correct answer |
slots.overlaySlot |
ReactNode |
undefined |
Extra overlay (e.g. sparkle effects) |
All apps are registered in packages/shared/src/lib/cant-apps.ts with their name, description, theme colors, icon SVG content, and cross-promo text. The CantSeriesGrid component renders cross-links on landing pages (variant="full") and play lobbies (variant="compact").
lib/theme.ts— each app has its own color palettecomponents/app-theme-wrapper.tsx— per-app panel labels, styling, and animationslib/shiki.ts— apps add language support as needed (CSS, HTML, Dockerfile, YAML, etc.)- Challenge data and category definitions
- Landing pages and app-specific features (viewer, playground, inspector, explorer, changelog)
icon.tsx,apple-icon.tsx,public/icon.svg— each app's branded icon
-
Copy an existing app as a starting point:
cp -r apps/cant-resize apps/cant-newapp
-
Update
apps/cant-newapp/package.json:- Change
nametocant-newapp - Keep
@cant/sharedas a workspace dependency
- Change
-
Customize:
lib/theme.ts— your app's color palettecomponents/app-theme-wrapper.tsx— panel labels, styling, checkmark animationlib/learn/categories.ts— your challenge categorieslib/learn/challenges/— your challenge contentapp/page.tsx— your landing pageapp/icon.tsx,app/apple-icon.tsx,public/icon.svg— your app icon- Metadata in
app/layout.tsx
-
Wire up
AppThemeWrapperinapp/layout.tsx(see AppThemeProvider section above). -
Register the app in
packages/shared/src/lib/cant-apps.tswith name, colors, and icon SVG content. -
Add root scripts to
package.json:"dev:newapp": "turbo dev --filter=cant-newapp", "build:newapp": "turbo build --filter=cant-newapp"
-
Create
apps/cant-newapp/Dockerfile(copy from an existing app, replace the app name). -
Run
pnpm installto link the workspace.
Run all checks and fix any issues:
pnpm lint
pnpm typecheck
pnpm format:checkIf formatting fails, run npx prettier --write . from the app directory and include the changes.
- Use pnpm, not npm
- No em dashes in any text (user-facing, comments, JSDoc, metadata). Use commas, periods, colons, or "and" instead
- Prefer MUI's
sxbreakpoint objects overuseMediaQueryfor responsive styling - Keep challenge explanations factually accurate and natural-sounding
- Conventional commits:
feat:,fix:,chore:,docs:
Each app deploys as a Docker container via Coolify with selective rebuilds per app. See docs/coolify-deployment.md for setup, webhook configuration, and watch paths.
MIT